diff --git a/.buildkite/ftr_oblt_serverless_configs.yml b/.buildkite/ftr_oblt_serverless_configs.yml index fbf0406f37be4..75909e7c21c46 100644 --- a/.buildkite/ftr_oblt_serverless_configs.yml +++ b/.buildkite/ftr_oblt_serverless_configs.yml @@ -6,6 +6,10 @@ disabled: - x-pack/test_serverless/functional/test_suites/observability/cypress/config_headless.ts - x-pack/test_serverless/functional/test_suites/observability/cypress/config_runner.ts + # serverless config files that run deployment-agnostic tests + # Failing https://github.com/elastic/kibana/issues/195811 + - x-pack/test/api_integration/deployment_agnostic/configs/serverless/oblt.serverless.config.ts + defaultQueue: 'n2-4-spot' enabled: - x-pack/test_serverless/api_integration/test_suites/observability/config.ts @@ -25,5 +29,3 @@ enabled: - x-pack/test_serverless/functional/test_suites/observability/common_configs/config.group5.ts - x-pack/test_serverless/functional/test_suites/observability/common_configs/config.group6.ts - x-pack/test_serverless/functional/test_suites/observability/config.screenshots.ts - # serverless config files that run deployment-agnostic tests - - x-pack/test/api_integration/deployment_agnostic/configs/serverless/oblt.serverless.config.ts diff --git a/.buildkite/ftr_platform_stateful_configs.yml b/.buildkite/ftr_platform_stateful_configs.yml index bc564624f8a5e..092cdb12a19f6 100644 --- a/.buildkite/ftr_platform_stateful_configs.yml +++ b/.buildkite/ftr_platform_stateful_configs.yml @@ -34,7 +34,9 @@ disabled: # Cypress configs, for now these are still run manually - x-pack/test/fleet_cypress/cli_config.ts + - x-pack/test/fleet_cypress/cli_config.space_awareness.ts - x-pack/test/fleet_cypress/config.ts + - x-pack/test/fleet_cypress/config.space_awareness.ts - x-pack/test/fleet_cypress/visual_config.ts defaultQueue: 'n2-4-spot' @@ -272,6 +274,7 @@ enabled: - x-pack/test/functional/config.upgrade_assistant.ts - x-pack/test/functional_cloud/config.ts - x-pack/test/functional_solution_sidenav/config.ts + - x-pack/test/functional_search/config.ts - x-pack/test/kubernetes_security/basic/config.ts - x-pack/test/licensing_plugin/config.public.ts - x-pack/test/licensing_plugin/config.ts diff --git a/.buildkite/ftr_search_serverless_configs.yml b/.buildkite/ftr_search_serverless_configs.yml index e6efee5860806..413558bffa0fe 100644 --- a/.buildkite/ftr_search_serverless_configs.yml +++ b/.buildkite/ftr_search_serverless_configs.yml @@ -1,6 +1,10 @@ disabled: # Base config files, only necessary to inform config finding script + # serverless config files that run deployment-agnostic tests + # Failing https://github.com/elastic/kibana/issues/195811 + - x-pack/test/api_integration/deployment_agnostic/configs/serverless/search.serverless.config.ts + defaultQueue: 'n2-4-spot' enabled: - x-pack/test_serverless/api_integration/test_suites/search/config.ts @@ -18,5 +22,3 @@ enabled: - x-pack/test_serverless/functional/test_suites/search/common_configs/config.group4.ts - x-pack/test_serverless/functional/test_suites/search/common_configs/config.group5.ts - x-pack/test_serverless/functional/test_suites/search/common_configs/config.group6.ts - # serverless config files that run deployment-agnostic tests - - x-pack/test/api_integration/deployment_agnostic/configs/serverless/search.serverless.config.ts diff --git a/.buildkite/ftr_security_serverless_configs.yml b/.buildkite/ftr_security_serverless_configs.yml index 6d42c030b2d4f..caf9fcc5ac92a 100644 --- a/.buildkite/ftr_security_serverless_configs.yml +++ b/.buildkite/ftr_security_serverless_configs.yml @@ -20,6 +20,10 @@ disabled: - x-pack/test_serverless/functional/config.base.ts - x-pack/test_serverless/shared/config.base.ts + # serverless config files that run deployment-agnostic tests + # Failing https://github.com/elastic/kibana/issues/195811 + - x-pack/test/api_integration/deployment_agnostic/configs/serverless/security.serverless.config.ts + defaultQueue: 'n2-4-spot' enabled: - x-pack/test_serverless/api_integration/test_suites/security/config.ts @@ -100,5 +104,3 @@ enabled: - x-pack/test/security_solution_api_integration/test_suites/edr_workflows/response_actions/trial_license_complete_tier/configs/serverless.config.ts - x-pack/test/security_solution_endpoint/configs/serverless.endpoint.config.ts - x-pack/test/security_solution_endpoint/configs/serverless.integrations.config.ts - # serverless config files that run deployment-agnostic tests - - x-pack/test/api_integration/deployment_agnostic/configs/serverless/security.serverless.config.ts diff --git a/.buildkite/pipeline-resource-definitions/_templates/_new_pipeline.yml b/.buildkite/pipeline-resource-definitions/_templates/_new_pipeline.yml index f33e738882693..6ef0d7652b964 100644 --- a/.buildkite/pipeline-resource-definitions/_templates/_new_pipeline.yml +++ b/.buildkite/pipeline-resource-definitions/_templates/_new_pipeline.yml @@ -70,3 +70,5 @@ spec: # Optionally, set schedule-specific env-vars here env: SCHEDULED: 'true' + tags: + - kibana diff --git a/.buildkite/pipeline-resource-definitions/kibana-api-docs.yml b/.buildkite/pipeline-resource-definitions/kibana-api-docs.yml index 5368b60314e47..26ff7242dac65 100644 --- a/.buildkite/pipeline-resource-definitions/kibana-api-docs.yml +++ b/.buildkite/pipeline-resource-definitions/kibana-api-docs.yml @@ -49,3 +49,5 @@ spec: cronline: 0 0 * * * America/New_York message: Daily build branch: main + tags: + - kibana diff --git a/.buildkite/pipeline-resource-definitions/kibana-apis-capacity-testing-daily.yml b/.buildkite/pipeline-resource-definitions/kibana-apis-capacity-testing-daily.yml index c52e6203485f4..244a0351de0be 100644 --- a/.buildkite/pipeline-resource-definitions/kibana-apis-capacity-testing-daily.yml +++ b/.buildkite/pipeline-resource-definitions/kibana-apis-capacity-testing-daily.yml @@ -47,3 +47,5 @@ spec: cronline: 0 1/3 * * * Europe/Berlin message: Capacity every 3h testing branch: main + tags: + - kibana diff --git a/.buildkite/pipeline-resource-definitions/kibana-artifacts-container-image.yml b/.buildkite/pipeline-resource-definitions/kibana-artifacts-container-image.yml index eff970c69af6b..eb86f8d7aab2a 100644 --- a/.buildkite/pipeline-resource-definitions/kibana-artifacts-container-image.yml +++ b/.buildkite/pipeline-resource-definitions/kibana-artifacts-container-image.yml @@ -42,3 +42,6 @@ spec: access_level: MANAGE_BUILD_AND_READ kibana-tech-leads: access_level: MANAGE_BUILD_AND_READ + tags: + - kibana + - kibana-serverless-release diff --git a/.buildkite/pipeline-resource-definitions/kibana-artifacts-snapshot.yml b/.buildkite/pipeline-resource-definitions/kibana-artifacts-snapshot.yml index e1c40f690f4ec..f994f0cba33c3 100644 --- a/.buildkite/pipeline-resource-definitions/kibana-artifacts-snapshot.yml +++ b/.buildkite/pipeline-resource-definitions/kibana-artifacts-snapshot.yml @@ -43,3 +43,5 @@ spec: access_level: MANAGE_BUILD_AND_READ kibana-tech-leads: access_level: MANAGE_BUILD_AND_READ + tags: + - kibana diff --git a/.buildkite/pipeline-resource-definitions/kibana-artifacts-staging.yml b/.buildkite/pipeline-resource-definitions/kibana-artifacts-staging.yml index 71bcc4079c50d..1d7b0488c7b3f 100644 --- a/.buildkite/pipeline-resource-definitions/kibana-artifacts-staging.yml +++ b/.buildkite/pipeline-resource-definitions/kibana-artifacts-staging.yml @@ -43,3 +43,5 @@ spec: access_level: MANAGE_BUILD_AND_READ kibana-tech-leads: access_level: MANAGE_BUILD_AND_READ + tags: + - kibana diff --git a/.buildkite/pipeline-resource-definitions/kibana-artifacts-trigger.yml b/.buildkite/pipeline-resource-definitions/kibana-artifacts-trigger.yml index 8a9585762de83..f08e505b9aabb 100644 --- a/.buildkite/pipeline-resource-definitions/kibana-artifacts-trigger.yml +++ b/.buildkite/pipeline-resource-definitions/kibana-artifacts-trigger.yml @@ -44,3 +44,5 @@ spec: access_level: MANAGE_BUILD_AND_READ kibana-tech-leads: access_level: MANAGE_BUILD_AND_READ + tags: + - kibana diff --git a/.buildkite/pipeline-resource-definitions/kibana-chrome-forward-testing.yml b/.buildkite/pipeline-resource-definitions/kibana-chrome-forward-testing.yml index beeb6152509b6..15265da35f390 100644 --- a/.buildkite/pipeline-resource-definitions/kibana-chrome-forward-testing.yml +++ b/.buildkite/pipeline-resource-definitions/kibana-chrome-forward-testing.yml @@ -47,3 +47,5 @@ spec: cronline: 0 12 * * * message: Daily Chrome Forward Testing branch: main + tags: + - kibana diff --git a/.buildkite/pipeline-resource-definitions/kibana-codeql.yml b/.buildkite/pipeline-resource-definitions/kibana-codeql.yml index 3da2c9137c4e0..68a41a547a64a 100644 --- a/.buildkite/pipeline-resource-definitions/kibana-codeql.yml +++ b/.buildkite/pipeline-resource-definitions/kibana-codeql.yml @@ -32,3 +32,5 @@ spec: access_level: MANAGE_BUILD_AND_READ everyone: access_level: READ_ONLY + tags: + - kibana diff --git a/.buildkite/pipeline-resource-definitions/kibana-coverage-daily.yml b/.buildkite/pipeline-resource-definitions/kibana-coverage-daily.yml index c73a276a6d786..4192bb9186589 100644 --- a/.buildkite/pipeline-resource-definitions/kibana-coverage-daily.yml +++ b/.buildkite/pipeline-resource-definitions/kibana-coverage-daily.yml @@ -48,3 +48,5 @@ spec: cronline: 0 5 * * * message: Daily 6 am UTC branch: main + tags: + - kibana diff --git a/.buildkite/pipeline-resource-definitions/kibana-deploy-project.yml b/.buildkite/pipeline-resource-definitions/kibana-deploy-project.yml new file mode 100644 index 0000000000000..3c1bdc00ba371 --- /dev/null +++ b/.buildkite/pipeline-resource-definitions/kibana-deploy-project.yml @@ -0,0 +1,44 @@ +# yaml-language-server: $schema=https://gist.githubusercontent.com/elasticmachine/988b80dae436cafea07d9a4a460a011d/raw/rre.schema.json +apiVersion: backstage.io/v1alpha1 +kind: Resource +metadata: + name: bk-kibana-deploy-project-from-pr + description: 'Builds and deploys a Kibana serverless project from a PR' + links: + - url: 'https://buildkite.com/elastic/kibana-deploy-project-from-pr' + title: Pipeline link +spec: + type: buildkite-pipeline + system: buildkite + owner: 'group:kibana-operations' + implementation: + apiVersion: buildkite.elastic.dev/v1 + kind: Pipeline + metadata: + name: kibana / deploy project from PR + description: 'Builds and deploys a Kibana serverless project from a PR' + spec: + env: + ELASTIC_SLACK_NOTIFICATIONS_ENABLED: 'false' + + allow_rebuilds: false + branch_configuration: main + default_branch: main + repository: elastic/kibana + pipeline_file: .buildkite/pipelines/serverless_deployment/project-build-and-deploy-pr.yml + skip_intermediate_builds: true + provider_settings: + prefix_pull_request_fork_branch_names: false + skip_pull_request_builds_for_existing_commits: true + trigger_mode: none + teams: + kibana-operations: + access_level: MANAGE_BUILD_AND_READ + appex-qa: + access_level: MANAGE_BUILD_AND_READ + kibana-tech-leads: + access_level: MANAGE_BUILD_AND_READ + everyone: + access_level: BUILD_AND_READ + tags: + - kibana diff --git a/.buildkite/pipeline-resource-definitions/kibana-es-forward-testing.yml b/.buildkite/pipeline-resource-definitions/kibana-es-forward-testing.yml index 2d87415841df6..fa4ee2d263873 100644 --- a/.buildkite/pipeline-resource-definitions/kibana-es-forward-testing.yml +++ b/.buildkite/pipeline-resource-definitions/kibana-es-forward-testing.yml @@ -40,3 +40,5 @@ spec: access_level: MANAGE_BUILD_AND_READ everyone: access_level: BUILD_AND_READ + tags: + - kibana diff --git a/.buildkite/pipeline-resource-definitions/kibana-es-serverless-snapshots.yml b/.buildkite/pipeline-resource-definitions/kibana-es-serverless-snapshots.yml index 60bedaafba586..6ba182ccd393e 100644 --- a/.buildkite/pipeline-resource-definitions/kibana-es-serverless-snapshots.yml +++ b/.buildkite/pipeline-resource-definitions/kibana-es-serverless-snapshots.yml @@ -53,3 +53,6 @@ spec: env: PUBLISH_DOCKER_TAG: 'true' branch: main + tags: + - kibana + - kibana-serverless-release diff --git a/.buildkite/pipeline-resource-definitions/kibana-es-snapshots.yml b/.buildkite/pipeline-resource-definitions/kibana-es-snapshots.yml index 1c041d7016f44..d386542fbdf0c 100644 --- a/.buildkite/pipeline-resource-definitions/kibana-es-snapshots.yml +++ b/.buildkite/pipeline-resource-definitions/kibana-es-snapshots.yml @@ -46,21 +46,23 @@ spec: access_level: MANAGE_BUILD_AND_READ schedules: Daily build (main): - cronline: 0 9 * * * America/New_York + cronline: 0 22 * * * America/New_York message: Daily build branch: main Daily build (8.x): - cronline: 0 9 * * * America/New_York + cronline: 0 22 * * * America/New_York message: Daily build branch: '8.x' Daily build (8.15): - cronline: 0 9 * * * America/New_York + cronline: 0 22 * * * America/New_York message: Daily build branch: '8.15' Daily build (7.17): - cronline: 0 9 * * * America/New_York + cronline: 0 22 * * * America/New_York message: Daily build branch: '7.17' + tags: + - kibana --- # yaml-language-server: $schema=https://gist.githubusercontent.com/elasticmachine/988b80dae436cafea07d9a4a460a011d/raw/rre.schema.json apiVersion: backstage.io/v1alpha1 @@ -108,6 +110,8 @@ spec: access_level: MANAGE_BUILD_AND_READ kibana-tech-leads: access_level: MANAGE_BUILD_AND_READ + tags: + - kibana --- # yaml-language-server: $schema=https://gist.githubusercontent.com/elasticmachine/988b80dae436cafea07d9a4a460a011d/raw/rre.schema.json apiVersion: backstage.io/v1alpha1 @@ -156,3 +160,5 @@ spec: access_level: MANAGE_BUILD_AND_READ kibana-tech-leads: access_level: MANAGE_BUILD_AND_READ + tags: + - kibana diff --git a/.buildkite/pipeline-resource-definitions/kibana-esql-grammar-sync.yml b/.buildkite/pipeline-resource-definitions/kibana-esql-grammar-sync.yml index 952babf7d580d..8cc4b54a5ce0c 100644 --- a/.buildkite/pipeline-resource-definitions/kibana-esql-grammar-sync.yml +++ b/.buildkite/pipeline-resource-definitions/kibana-esql-grammar-sync.yml @@ -51,3 +51,5 @@ spec: cronline: 0 0 * * 1 America/New_York message: Weekly build branch: main + tags: + - kibana diff --git a/.buildkite/pipeline-resource-definitions/kibana-fips-daily.yml b/.buildkite/pipeline-resource-definitions/kibana-fips-daily.yml index b64521858c1f6..bedb81cccc5a4 100644 --- a/.buildkite/pipeline-resource-definitions/kibana-fips-daily.yml +++ b/.buildkite/pipeline-resource-definitions/kibana-fips-daily.yml @@ -38,3 +38,5 @@ spec: access_level: MANAGE_BUILD_AND_READ everyone: access_level: READ_ONLY + tags: + - kibana diff --git a/.buildkite/pipeline-resource-definitions/kibana-flaky.yml b/.buildkite/pipeline-resource-definitions/kibana-flaky.yml index 82797c03f2378..f1c348e059209 100644 --- a/.buildkite/pipeline-resource-definitions/kibana-flaky.yml +++ b/.buildkite/pipeline-resource-definitions/kibana-flaky.yml @@ -39,3 +39,5 @@ spec: access_level: MANAGE_BUILD_AND_READ kibana-tech-leads: access_level: MANAGE_BUILD_AND_READ + tags: + - kibana diff --git a/.buildkite/pipeline-resource-definitions/kibana-fleet-packages-daily.yml b/.buildkite/pipeline-resource-definitions/kibana-fleet-packages-daily.yml index 8805fef47f914..d948460513c8e 100644 --- a/.buildkite/pipeline-resource-definitions/kibana-fleet-packages-daily.yml +++ b/.buildkite/pipeline-resource-definitions/kibana-fleet-packages-daily.yml @@ -47,3 +47,5 @@ spec: message: Single user daily test env: {} branch: main + tags: + - kibana diff --git a/.buildkite/pipeline-resource-definitions/kibana-migration-staging.yml b/.buildkite/pipeline-resource-definitions/kibana-migration-staging.yml index b999babc24fc8..980fee4db5671 100644 --- a/.buildkite/pipeline-resource-definitions/kibana-migration-staging.yml +++ b/.buildkite/pipeline-resource-definitions/kibana-migration-staging.yml @@ -30,3 +30,5 @@ spec: access_level: MANAGE_BUILD_AND_READ everyone: access_level: BUILD_AND_READ + tags: + - kibana diff --git a/.buildkite/pipeline-resource-definitions/kibana-on-merge-unsupported-ftrs.yml b/.buildkite/pipeline-resource-definitions/kibana-on-merge-unsupported-ftrs.yml index b9c6cb8970271..a6ddb28309987 100644 --- a/.buildkite/pipeline-resource-definitions/kibana-on-merge-unsupported-ftrs.yml +++ b/.buildkite/pipeline-resource-definitions/kibana-on-merge-unsupported-ftrs.yml @@ -44,3 +44,5 @@ spec: access_level: MANAGE_BUILD_AND_READ kibana-tech-leads: access_level: MANAGE_BUILD_AND_READ + tags: + - kibana diff --git a/.buildkite/pipeline-resource-definitions/kibana-on-merge.yml b/.buildkite/pipeline-resource-definitions/kibana-on-merge.yml index 6fe305979652e..e524adc786c0e 100644 --- a/.buildkite/pipeline-resource-definitions/kibana-on-merge.yml +++ b/.buildkite/pipeline-resource-definitions/kibana-on-merge.yml @@ -47,3 +47,6 @@ spec: access_level: MANAGE_BUILD_AND_READ kibana-tech-leads: access_level: MANAGE_BUILD_AND_READ + tags: + - kibana + - kibana-serverless-release diff --git a/.buildkite/pipeline-resource-definitions/kibana-performance-daily.yml b/.buildkite/pipeline-resource-definitions/kibana-performance-daily.yml index 9ed561c9cfdbe..915cce93a2481 100644 --- a/.buildkite/pipeline-resource-definitions/kibana-performance-daily.yml +++ b/.buildkite/pipeline-resource-definitions/kibana-performance-daily.yml @@ -48,3 +48,5 @@ spec: cronline: 0 */3 * * * Europe/Berlin message: Single user daily test branch: main + tags: + - kibana diff --git a/.buildkite/pipeline-resource-definitions/kibana-performance-data-set-extraction-daily.yml b/.buildkite/pipeline-resource-definitions/kibana-performance-data-set-extraction-daily.yml index aa38564fd963b..3fe79832d35fd 100644 --- a/.buildkite/pipeline-resource-definitions/kibana-performance-data-set-extraction-daily.yml +++ b/.buildkite/pipeline-resource-definitions/kibana-performance-data-set-extraction-daily.yml @@ -47,3 +47,5 @@ spec: cronline: 0 3/8 * * * Europe/Berlin message: Extract APM traces branch: main + tags: + - kibana diff --git a/.buildkite/pipeline-resource-definitions/kibana-pointer-compression.yml b/.buildkite/pipeline-resource-definitions/kibana-pointer-compression.yml index 5a23fc95d9971..bcc94453b14e2 100644 --- a/.buildkite/pipeline-resource-definitions/kibana-pointer-compression.yml +++ b/.buildkite/pipeline-resource-definitions/kibana-pointer-compression.yml @@ -36,3 +36,5 @@ spec: access_level: MANAGE_BUILD_AND_READ everyone: access_level: READ_ONLY + tags: + - kibana diff --git a/.buildkite/pipeline-resource-definitions/kibana-pr.yml b/.buildkite/pipeline-resource-definitions/kibana-pr.yml index 4d6275843327e..2ce36f6799b5a 100644 --- a/.buildkite/pipeline-resource-definitions/kibana-pr.yml +++ b/.buildkite/pipeline-resource-definitions/kibana-pr.yml @@ -47,3 +47,5 @@ spec: access_level: MANAGE_BUILD_AND_READ kibana-tech-leads: access_level: MANAGE_BUILD_AND_READ + tags: + - kibana diff --git a/.buildkite/pipeline-resource-definitions/kibana-purge-cloud-deployments.yml b/.buildkite/pipeline-resource-definitions/kibana-purge-cloud-deployments.yml index 3b5d3fd84fad5..9124d001d6f70 100644 --- a/.buildkite/pipeline-resource-definitions/kibana-purge-cloud-deployments.yml +++ b/.buildkite/pipeline-resource-definitions/kibana-purge-cloud-deployments.yml @@ -49,3 +49,5 @@ spec: access_level: MANAGE_BUILD_AND_READ everyone: access_level: BUILD_AND_READ + tags: + - kibana diff --git a/.buildkite/pipeline-resource-definitions/kibana-serverless-emergency-release.yml b/.buildkite/pipeline-resource-definitions/kibana-serverless-emergency-release.yml index 5911095262ac1..62b05bc49dae6 100644 --- a/.buildkite/pipeline-resource-definitions/kibana-serverless-emergency-release.yml +++ b/.buildkite/pipeline-resource-definitions/kibana-serverless-emergency-release.yml @@ -28,3 +28,6 @@ spec: access_level: BUILD_AND_READ everyone: access_level: READ_ONLY + tags: + - kibana + - kibana-serverless-release diff --git a/.buildkite/pipeline-resource-definitions/kibana-serverless-quality-gates-emergency.yml b/.buildkite/pipeline-resource-definitions/kibana-serverless-quality-gates-emergency.yml index ba053d7c44da6..ef04fd324b31a 100644 --- a/.buildkite/pipeline-resource-definitions/kibana-serverless-quality-gates-emergency.yml +++ b/.buildkite/pipeline-resource-definitions/kibana-serverless-quality-gates-emergency.yml @@ -31,3 +31,6 @@ spec: access_level: BUILD_AND_READ everyone: access_level: READ_ONLY + tags: + - kibana + - kibana-serverless-release diff --git a/.buildkite/pipeline-resource-definitions/kibana-serverless-quality-gates.yml b/.buildkite/pipeline-resource-definitions/kibana-serverless-quality-gates.yml index 1f57f2ca83250..e9ea3d02b8968 100644 --- a/.buildkite/pipeline-resource-definitions/kibana-serverless-quality-gates.yml +++ b/.buildkite/pipeline-resource-definitions/kibana-serverless-quality-gates.yml @@ -31,3 +31,6 @@ spec: access_level: BUILD_AND_READ everyone: access_level: READ_ONLY + tags: + - kibana + - kibana-serverless-release diff --git a/.buildkite/pipeline-resource-definitions/kibana-serverless-release-testing.yml b/.buildkite/pipeline-resource-definitions/kibana-serverless-release-testing.yml index fe3fdaf49c748..5276871fa1c9f 100644 --- a/.buildkite/pipeline-resource-definitions/kibana-serverless-release-testing.yml +++ b/.buildkite/pipeline-resource-definitions/kibana-serverless-release-testing.yml @@ -44,3 +44,6 @@ spec: access_level: MANAGE_BUILD_AND_READ kibana-tech-leads: access_level: MANAGE_BUILD_AND_READ + tags: + - kibana + - kibana-serverless-release diff --git a/.buildkite/pipeline-resource-definitions/kibana-serverless-release.yml b/.buildkite/pipeline-resource-definitions/kibana-serverless-release.yml index 057a31c47190a..e1457f10420f7 100644 --- a/.buildkite/pipeline-resource-definitions/kibana-serverless-release.yml +++ b/.buildkite/pipeline-resource-definitions/kibana-serverless-release.yml @@ -46,3 +46,6 @@ spec: env: AUTO_SELECT_COMMIT: 'true' branch: main + tags: + - kibana + - kibana-serverless-release diff --git a/.buildkite/pipeline-resource-definitions/locations.yml b/.buildkite/pipeline-resource-definitions/locations.yml index ab584690ca8d1..ce0ab7750d489 100644 --- a/.buildkite/pipeline-resource-definitions/locations.yml +++ b/.buildkite/pipeline-resource-definitions/locations.yml @@ -16,6 +16,7 @@ spec: - https://github.com/elastic/kibana/blob/main/.buildkite/pipeline-resource-definitions/kibana-chrome-forward-testing.yml - https://github.com/elastic/kibana/blob/main/.buildkite/pipeline-resource-definitions/kibana-codeql.yml - https://github.com/elastic/kibana/blob/main/.buildkite/pipeline-resource-definitions/kibana-coverage-daily.yml + - https://github.com/elastic/kibana/blob/main/.buildkite/pipeline-resource-definitions/kibana-deploy-project.yml - https://github.com/elastic/kibana/blob/main/.buildkite/pipeline-resource-definitions/kibana-es-forward-testing.yml - https://github.com/elastic/kibana/blob/main/.buildkite/pipeline-resource-definitions/kibana-es-serverless-snapshots.yml - https://github.com/elastic/kibana/blob/main/.buildkite/pipeline-resource-definitions/kibana-es-snapshots.yml diff --git a/.buildkite/pipeline-resource-definitions/scalability_testing-daily.yml b/.buildkite/pipeline-resource-definitions/scalability_testing-daily.yml index 06f2f2dd6634b..162bb6220ea1c 100644 --- a/.buildkite/pipeline-resource-definitions/scalability_testing-daily.yml +++ b/.buildkite/pipeline-resource-definitions/scalability_testing-daily.yml @@ -47,3 +47,5 @@ spec: cronline: 0 6 * * * Europe/Berlin message: Scalability daily benchmarking branch: main + tags: + - kibana diff --git a/.buildkite/pipeline-resource-definitions/security-solution-ess/security-solution-ess.yml b/.buildkite/pipeline-resource-definitions/security-solution-ess/security-solution-ess.yml index b22891cbe9d2d..239bd74c66922 100644 --- a/.buildkite/pipeline-resource-definitions/security-solution-ess/security-solution-ess.yml +++ b/.buildkite/pipeline-resource-definitions/security-solution-ess/security-solution-ess.yml @@ -35,3 +35,6 @@ spec: access_level: MANAGE_BUILD_AND_READ everyone: access_level: BUILD_AND_READ + tags: + - kibana + - security-solution diff --git a/.buildkite/pipeline-resource-definitions/security-solution-quality-gate/kibana-serverless-security-solution-quality-gate-defend-workflows.yml b/.buildkite/pipeline-resource-definitions/security-solution-quality-gate/kibana-serverless-security-solution-quality-gate-defend-workflows.yml index d5e32ca55172c..d4d2541f1c4ad 100644 --- a/.buildkite/pipeline-resource-definitions/security-solution-quality-gate/kibana-serverless-security-solution-quality-gate-defend-workflows.yml +++ b/.buildkite/pipeline-resource-definitions/security-solution-quality-gate/kibana-serverless-security-solution-quality-gate-defend-workflows.yml @@ -36,3 +36,6 @@ spec: access_level: MANAGE_BUILD_AND_READ everyone: access_level: BUILD_AND_READ + tags: + - kibana + - security-solution diff --git a/.buildkite/pipeline-resource-definitions/security-solution-quality-gate/kibana-serverless-security-solution-quality-gate-detection-engine.yml b/.buildkite/pipeline-resource-definitions/security-solution-quality-gate/kibana-serverless-security-solution-quality-gate-detection-engine.yml index 8dc4265b3e6f4..77361eed441e6 100644 --- a/.buildkite/pipeline-resource-definitions/security-solution-quality-gate/kibana-serverless-security-solution-quality-gate-detection-engine.yml +++ b/.buildkite/pipeline-resource-definitions/security-solution-quality-gate/kibana-serverless-security-solution-quality-gate-detection-engine.yml @@ -36,3 +36,6 @@ spec: access_level: MANAGE_BUILD_AND_READ everyone: access_level: BUILD_AND_READ + tags: + - kibana + - security-solution diff --git a/.buildkite/pipeline-resource-definitions/security-solution-quality-gate/kibana-serverless-security-solution-quality-gate-entity-analytics.yml b/.buildkite/pipeline-resource-definitions/security-solution-quality-gate/kibana-serverless-security-solution-quality-gate-entity-analytics.yml index 9d5bba5f40d1d..49338bf7b6d32 100644 --- a/.buildkite/pipeline-resource-definitions/security-solution-quality-gate/kibana-serverless-security-solution-quality-gate-entity-analytics.yml +++ b/.buildkite/pipeline-resource-definitions/security-solution-quality-gate/kibana-serverless-security-solution-quality-gate-entity-analytics.yml @@ -36,3 +36,6 @@ spec: access_level: MANAGE_BUILD_AND_READ everyone: access_level: BUILD_AND_READ + tags: + - kibana + - security-solution diff --git a/.buildkite/pipeline-resource-definitions/security-solution-quality-gate/kibana-serverless-security-solution-quality-gate-explore.yml b/.buildkite/pipeline-resource-definitions/security-solution-quality-gate/kibana-serverless-security-solution-quality-gate-explore.yml index cd2739fe4a6fb..ee8cf00a755f9 100644 --- a/.buildkite/pipeline-resource-definitions/security-solution-quality-gate/kibana-serverless-security-solution-quality-gate-explore.yml +++ b/.buildkite/pipeline-resource-definitions/security-solution-quality-gate/kibana-serverless-security-solution-quality-gate-explore.yml @@ -36,3 +36,6 @@ spec: access_level: MANAGE_BUILD_AND_READ everyone: access_level: BUILD_AND_READ + tags: + - kibana + - security-solution diff --git a/.buildkite/pipeline-resource-definitions/security-solution-quality-gate/kibana-serverless-security-solution-quality-gate-gen-ai.yml b/.buildkite/pipeline-resource-definitions/security-solution-quality-gate/kibana-serverless-security-solution-quality-gate-gen-ai.yml index 1aeeefe2a0ad8..f22e321176eb0 100644 --- a/.buildkite/pipeline-resource-definitions/security-solution-quality-gate/kibana-serverless-security-solution-quality-gate-gen-ai.yml +++ b/.buildkite/pipeline-resource-definitions/security-solution-quality-gate/kibana-serverless-security-solution-quality-gate-gen-ai.yml @@ -36,3 +36,6 @@ spec: access_level: MANAGE_BUILD_AND_READ everyone: access_level: BUILD_AND_READ + tags: + - kibana + - security-solution diff --git a/.buildkite/pipeline-resource-definitions/security-solution-quality-gate/kibana-serverless-security-solution-quality-gate-investigations.yml b/.buildkite/pipeline-resource-definitions/security-solution-quality-gate/kibana-serverless-security-solution-quality-gate-investigations.yml index 955bcf24b1e63..7de3b5f8cc282 100644 --- a/.buildkite/pipeline-resource-definitions/security-solution-quality-gate/kibana-serverless-security-solution-quality-gate-investigations.yml +++ b/.buildkite/pipeline-resource-definitions/security-solution-quality-gate/kibana-serverless-security-solution-quality-gate-investigations.yml @@ -36,3 +36,6 @@ spec: access_level: MANAGE_BUILD_AND_READ everyone: access_level: BUILD_AND_READ + tags: + - kibana + - security-solution diff --git a/.buildkite/pipeline-resource-definitions/security-solution-quality-gate/kibana-serverless-security-solution-quality-gate-rule-management.yml b/.buildkite/pipeline-resource-definitions/security-solution-quality-gate/kibana-serverless-security-solution-quality-gate-rule-management.yml index af0386076dd4d..4f095294f4422 100644 --- a/.buildkite/pipeline-resource-definitions/security-solution-quality-gate/kibana-serverless-security-solution-quality-gate-rule-management.yml +++ b/.buildkite/pipeline-resource-definitions/security-solution-quality-gate/kibana-serverless-security-solution-quality-gate-rule-management.yml @@ -36,3 +36,6 @@ spec: access_level: MANAGE_BUILD_AND_READ everyone: access_level: BUILD_AND_READ + tags: + - kibana + - security-solution diff --git a/.buildkite/pipeline-resource-definitions/trigger-version-dependent-jobs.yml b/.buildkite/pipeline-resource-definitions/trigger-version-dependent-jobs.yml index ea474356b137d..8dd486c3176ce 100644 --- a/.buildkite/pipeline-resource-definitions/trigger-version-dependent-jobs.yml +++ b/.buildkite/pipeline-resource-definitions/trigger-version-dependent-jobs.yml @@ -69,3 +69,5 @@ spec: env: TRIGGER_PIPELINE_SET: artifacts-trigger MESSAGE: Daily build + tags: + - kibana diff --git a/.buildkite/pipelines/es_serverless/verify_es_serverless_image.yml b/.buildkite/pipelines/es_serverless/verify_es_serverless_image.yml index cb0b63852ad00..ae50082726289 100644 --- a/.buildkite/pipelines/es_serverless/verify_es_serverless_image.yml +++ b/.buildkite/pipelines/es_serverless/verify_es_serverless_image.yml @@ -54,7 +54,7 @@ steps: env: FTR_CONFIGS_SCRIPT: "TEST_ES_SERVERLESS_IMAGE=$ES_SERVERLESS_IMAGE .buildkite/scripts/steps/test/ftr_configs.sh" JEST_INTEGRATION_SCRIPT: "TEST_ES_SERVERLESS_IMAGE=$ES_SERVERLESS_IMAGE .buildkite/scripts/steps/test/jest_integration.sh" - FTR_CONFIG_PATTERNS: "**/test_serverless/**,**/test/security_solution_api_integration/**/serverless.config.ts" + FTR_CONFIG_PATTERNS: "**/test_serverless/**,**/test/security_solution_api_integration/**/serverless.config.ts,x-pack/test/api_integration/deployment_agnostic/configs/serverless/**" FTR_EXTRA_ARGS: "$FTR_EXTRA_ARGS" LIMIT_CONFIG_TYPE: "functional,integration" retry: diff --git a/.buildkite/pipelines/serverless_deployment/project-build-and-deploy-pr.yml b/.buildkite/pipelines/serverless_deployment/project-build-and-deploy-pr.yml new file mode 100644 index 0000000000000..f7fc94ac444e1 --- /dev/null +++ b/.buildkite/pipelines/serverless_deployment/project-build-and-deploy-pr.yml @@ -0,0 +1,53 @@ +agents: + provider: gcp + image: family/kibana-ubuntu-2004 + imageProject: elastic-images-prod + +steps: + - command: .buildkite/scripts/lifecycle/pre_build.sh + label: Pre-Build + timeout_in_minutes: 10 + agents: + machineType: n2-standard-2 + retry: + automatic: + - exit_status: '*' + limit: 1 + + - wait: ~ + + - command: .buildkite/scripts/steps/build_kibana.sh + label: Build Kibana Distribution and Plugins + agents: + machineType: n2-standard-16 + preemptible: true + key: build + if: "build.env('KIBANA_BUILD_ID') == null || build.env('KIBANA_BUILD_ID') == ''" + timeout_in_minutes: 90 + retry: + automatic: + - exit_status: '-1' + limit: 3 + + - wait: ~ + + - command: .buildkite/scripts/steps/artifacts/docker_image.sh + label: 'Build Project Image' + key: build_project_image + agents: + machineType: n2-standard-16 + preemptible: true + timeout_in_minutes: 60 + retry: + automatic: + - exit_status: '-1' + limit: 3 + + - wait: ~ + + - command: .buildkite/scripts/steps/serverless/deploy.sh + label: 'Deploy Project' + agents: + machineType: n2-standard-4 + preemptible: true + timeout_in_minutes: 10 diff --git a/.buildkite/pull_requests.json b/.buildkite/pull_requests.json index 1f45c01042888..479265293cf22 100644 --- a/.buildkite/pull_requests.json +++ b/.buildkite/pull_requests.json @@ -30,7 +30,8 @@ "^\\.backportrc\\.json$", "^nav-kibana-dev\\.docnav\\.json$", "^src/dev/prs/kibana_qa_pr_list\\.json$", - "^\\.buildkite/pull_requests\\.json$" + "^\\.buildkite/pull_requests\\.json$", + "^\\.devcontainer/" ], "always_require_ci_on_changed": [ "^docs/developer/plugin-list.asciidoc$", @@ -46,6 +47,30 @@ "/__snapshots__/", "\\.test\\.(ts|tsx|js|jsx)" ] + }, + { + "repoOwner": "elastic", + "repoName": "kibana", + "pipelineSlug": "kibana-deploy-project", + + "enabled": true, + "allow_org_users": true, + "allowed_repo_permissions": ["admin", "write"], + "allowed_list": ["elastic-vault-github-plugin-prod[bot]"], + "set_commit_status": false, + "build_on_commit": false, + "build_on_comment": false, + "build_drafts": false, + "trigger_comment_regex": "^(?:(?:buildkite\\W+)?(?:deploy)\\W+(?:project))$", + "kibana_versions_check": true, + "kibana_build_reuse": true, + "kibana_build_reuse_pipeline_slugs": ["kibana-pull-request", "kibana-on-merge", "kibana-deploy-project"], + "kibana_build_reuse_regexes": [ + "^test/", + "^x-pack/test/", + "/__snapshots__/", + "\\.test\\.(ts|tsx|js|jsx)" + ] } ] } diff --git a/.buildkite/scripts/steps/cloud/build_and_deploy.sh b/.buildkite/scripts/steps/cloud/build_and_deploy.sh index 25e7d8fc631c9..220ab497aaf7b 100755 --- a/.buildkite/scripts/steps/cloud/build_and_deploy.sh +++ b/.buildkite/scripts/steps/cloud/build_and_deploy.sh @@ -51,7 +51,7 @@ fi if is_pr_with_label "ci:cloud-redeploy"; then echo "--- Shutdown Previous Deployment" CLOUD_DEPLOYMENT_ID=$(ecctl deployment list --output json | jq -r '.deployments[] | select(.name == "'$CLOUD_DEPLOYMENT_NAME'") | .id') - if [ -z "${CLOUD_DEPLOYMENT_ID}" ]; then + if [ -z "${CLOUD_DEPLOYMENT_ID}" ] || [ "${CLOUD_DEPLOYMENT_ID}" == "null" ]; then echo "No deployment to remove" else echo "Shutting down previous deployment..." diff --git a/.buildkite/scripts/steps/es_snapshots/promote.sh b/.buildkite/scripts/steps/es_snapshots/promote.sh index cf52f5e9ff650..5654d7bd3b8d3 100755 --- a/.buildkite/scripts/steps/es_snapshots/promote.sh +++ b/.buildkite/scripts/steps/es_snapshots/promote.sh @@ -16,4 +16,12 @@ ts-node "$(dirname "${0}")/promote_manifest.ts" "$ES_SNAPSHOT_MANIFEST" if [[ "$BUILDKITE_BRANCH" == "main" ]]; then echo "--- Trigger agent packer cache pipeline" ts-node .buildkite/scripts/steps/trigger_pipeline.ts kibana-agent-packer-cache main + cat << EOF | buildkite-agent pipeline upload +steps: + - label: "Builds Kibana VM images for cache update" + trigger: ci-vm-images + build: + env: + IMAGES_CONFIG="kibana/images.yml" +EOF fi diff --git a/.buildkite/scripts/steps/functional/fleet_cypress.sh b/.buildkite/scripts/steps/functional/fleet_cypress.sh index 43eb329f860b7..e050b73a91c3e 100755 --- a/.buildkite/scripts/steps/functional/fleet_cypress.sh +++ b/.buildkite/scripts/steps/functional/fleet_cypress.sh @@ -12,4 +12,4 @@ echo "--- Fleet Cypress tests (Chrome)" cd x-pack/plugins/fleet set +e -yarn cypress:run:reporter; status=$?; yarn junit:merge || :; exit $status +yarn cypress:run:reporter; status=$?; yarn cypress_space_awareness:run:reporter; space_status=$?; yarn junit:merge || :; [ "$status" -ne 0 ] && exit $status || [ "$space_status" -ne 0 ] && exit $space_status || exit 0 diff --git a/.buildkite/scripts/steps/serverless/deploy.sh b/.buildkite/scripts/steps/serverless/deploy.sh index d30723393dacd..2c7fd1fdf2e69 100644 --- a/.buildkite/scripts/steps/serverless/deploy.sh +++ b/.buildkite/scripts/steps/serverless/deploy.sh @@ -56,7 +56,7 @@ deploy() { PROJECT_ID=$(jq -r '[.items[] | select(.name == "'$PROJECT_NAME'")] | .[0].id' $PROJECT_EXISTS_LOGS) if is_pr_with_label "ci:project-redeploy"; then - if [ -z "${PROJECT_ID}" ]; then + if [ -z "${PROJECT_ID}" ] || [ "${PROJECT_ID}" == "null" ]; then echo "No project to remove" else echo "Shutting down previous project..." @@ -159,6 +159,7 @@ EOF } is_pr_with_label "ci:project-deploy-elasticsearch" && deploy "elasticsearch" +is_pr_with_label "ci:project-deploy-security" && deploy "security" if is_pr_with_label "ci:project-deploy-observability" ; then # Only deploy observability if the PR is targeting main if [[ "$BUILDKITE_PULL_REQUEST_BASE_BRANCH" == "main" ]]; then @@ -166,6 +167,5 @@ if is_pr_with_label "ci:project-deploy-observability" ; then buildkite-agent annotate --context obl-test-info --style info 'See linked [Deploy Serverless Kibana] issue in pull request for project deployment information' fi fi -is_pr_with_label "ci:project-deploy-security" && deploy "security" exit 0; diff --git a/.eslintrc.js b/.eslintrc.js index 797b84522df3f..f2e54b2d116ae 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -978,6 +978,7 @@ module.exports = { files: [ 'x-pack/plugins/observability_solution/**/!(*.stories.tsx|*.test.tsx|*.storybook_decorator.tsx|*.mock.tsx)', 'src/plugins/ai_assistant_management/**/!(*.stories.tsx|*.test.tsx|*.storybook_decorator.tsx|*.mock.tsx)', + 'x-pack/packages/observability/logs_overview/**/!(*.stories.tsx|*.test.tsx|*.storybook_decorator.tsx|*.mock.tsx)', ], rules: { '@kbn/i18n/strings_should_be_translated_with_i18n': 'warn', @@ -1814,9 +1815,23 @@ module.exports = { files: [ 'src/plugins/interactive_setup/**/*.{js,mjs,ts,tsx}', 'test/interactive_setup_api_integration/**/*.{js,mjs,ts,tsx}', + 'test/interactive_setup_functional/**/*.{js,mjs,ts,tsx}', + + 'packages/kbn-mock-idp-plugin/**/*.{js,mjs,ts,tsx}', + 'packages/kbn-mock-idp-utils/**/*.{js,mjs,ts,tsx}', + 'packages/kbn-security-hardening/**/*.{js,mjs,ts,tsx}', + 'packages/kbn-user-profile-components/**/*.{js,mjs,ts,tsx}', + 'x-pack/plugins/encrypted_saved_objects/**/*.{js,mjs,ts,tsx}', + 'x-pack/test/encrypted_saved_objects_api_integration/**/*.{js,mjs,ts,tsx}', + 'x-pack/plugins/security/**/*.{js,mjs,ts,tsx}', + 'x-pack/packages/security/**/*.{js,mjs,ts,tsx}', + 'x-pack/test/security_api_integration/**/*.{js,mjs,ts,tsx}', + 'x-pack/test/security_functional/**/*.{js,mjs,ts,tsx}', + 'x-pack/plugins/spaces/**/*.{js,mjs,ts,tsx}', + 'x-pack/test/spaces_api_integration/**/*.{js,mjs,ts,tsx}', ], rules: { '@typescript-eslint/consistent-type-imports': 1, diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index a20e12d88c352..10496d5351ef6 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -6,11 +6,11 @@ #### x-pack/test/alerting_api_integration/common/plugins/aad @elastic/response-ops -packages/kbn-ace @elastic/kibana-management x-pack/plugins/actions @elastic/response-ops x-pack/test/alerting_api_integration/common/plugins/actions_simulators @elastic/response-ops packages/kbn-actions-types @elastic/response-ops src/plugins/advanced_settings @elastic/appex-sharedux @elastic/kibana-management +x-pack/packages/kbn-ai-assistant @elastic/search-kibana src/plugins/ai_assistant_management/selection @elastic/obs-knowledge-team x-pack/packages/ml/aiops_change_point_detection @elastic/ml-ui x-pack/packages/ml/aiops_common @elastic/ml-ui @@ -652,6 +652,7 @@ x-pack/packages/observability/alerting_test_data @elastic/obs-ux-management-team x-pack/test/cases_api_integration/common/plugins/observability @elastic/response-ops x-pack/packages/observability/get_padded_alert_time_range_util @elastic/obs-ux-management-team x-pack/plugins/observability_solution/observability_logs_explorer @elastic/obs-ux-logs-team +x-pack/packages/observability/logs_overview @elastic/obs-ux-logs-team x-pack/plugins/observability_solution/observability_onboarding/e2e @elastic/obs-ux-logs-team @elastic/obs-ux-onboarding-team x-pack/plugins/observability_solution/observability_onboarding @elastic/obs-ux-logs-team @elastic/obs-ux-onboarding-team x-pack/plugins/observability_solution/observability @elastic/obs-ux-management-team @@ -1482,6 +1483,7 @@ x-pack/test/api_integration/apis/management/index_management/inference_endpoints /x-pack/test_serverless/api_integration/test_suites/search @elastic/search-kibana /x-pack/test_serverless/functional/page_objects/svl_api_keys.ts @elastic/search-kibana /x-pack/test_serverless/functional/page_objects/svl_search_* @elastic/search-kibana +/x-pack/test/functional_search/ @elastic/search-kibana # Management Experience - Deployment Management /x-pack/test_serverless/**/test_suites/common/index_management/ @elastic/kibana-management diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md index d07f60cf09253..737eedabadfa0 100644 --- a/.github/PULL_REQUEST_TEMPLATE.md +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -36,4 +36,6 @@ When forming the risk matrix, consider some of the following examples and how th ### For maintainers -- [ ] This was checked for breaking API changes and was [labeled appropriately](https://www.elastic.co/guide/en/kibana/master/contributing.html#kibana-release-notes-process) +- [ ] This was checked for breaking API changes and was [labeled appropriately](https://www.elastic.co/guide/en/kibana/master/contributing.html#_add_your_labels) +- [ ] This will appear in the **Release Notes** and follow the [guidelines](https://www.elastic.co/guide/en/kibana/master/contributing.html#kibana-release-notes-process) + diff --git a/.github/updatecli/values.d/ironbank.yml b/.github/updatecli/values.d/ironbank.yml new file mode 100644 index 0000000000000..fd1134eda376a --- /dev/null +++ b/.github/updatecli/values.d/ironbank.yml @@ -0,0 +1,2 @@ +config: + - path: src/dev/build/tasks/os_packages/docker_generator/templates/ironbank \ No newline at end of file diff --git a/.github/updatecli/values.d/scm.yml b/.github/updatecli/values.d/scm.yml new file mode 100644 index 0000000000000..34d902fb389d5 --- /dev/null +++ b/.github/updatecli/values.d/scm.yml @@ -0,0 +1,11 @@ +scm: + enabled: true + owner: elastic + repository: kibana + branch: main + commitusingapi: true + # begin updatecli-compose policy values + user: kibanamachine + email: 42973632+kibanamachine@users.noreply.github.com + # end updatecli-compose policy values + diff --git a/.github/updatecli/values.d/updatecli-compose.yml b/.github/updatecli/values.d/updatecli-compose.yml new file mode 100644 index 0000000000000..02df609f2a30c --- /dev/null +++ b/.github/updatecli/values.d/updatecli-compose.yml @@ -0,0 +1,3 @@ +spec: + files: + - "updatecli-compose.yaml" \ No newline at end of file diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml index e16dbcb261807..e80b3b2c73463 100644 --- a/.github/workflows/codeql.yml +++ b/.github/workflows/codeql.yml @@ -73,7 +73,9 @@ jobs: env: GITHUB_TOKEN: ${{secrets.KIBANAMACHINE_TOKEN}} SLACK_TOKEN: ${{secrets.CODE_SCANNING_SLACK_TOKEN}} - CODEQL_BRANCHES: 7.17,8.x,main + CODE_SCANNING_ES_HOST: ${{secrets.CODE_SCANNING_ES_HOST}} + CODE_SCANNING_ES_API_KEY: ${{secrets.CODE_SCANNING_ES_API_KEY}} + CODE_SCANNING_BRANCHES: 7.17,8.x,main run: | npm ci --omit=dev node codeql-alert diff --git a/.github/workflows/oblt-github-commands.yml b/.github/workflows/oblt-github-commands.yml index 443c0fa5f9071..48df40f3343d9 100644 --- a/.github/workflows/oblt-github-commands.yml +++ b/.github/workflows/oblt-github-commands.yml @@ -14,6 +14,7 @@ on: permissions: contents: read + issues: write pull-requests: write jobs: diff --git a/.github/workflows/updatecli-compose.yml b/.github/workflows/updatecli-compose.yml new file mode 100644 index 0000000000000..cbab42d3a63b1 --- /dev/null +++ b/.github/workflows/updatecli-compose.yml @@ -0,0 +1,38 @@ +--- +name: updatecli-compose + +on: + workflow_dispatch: + schedule: + - cron: '0 6 * * *' + +permissions: + contents: read + +jobs: + compose: + runs-on: ubuntu-latest + permissions: + contents: write + packages: read + pull-requests: write + steps: + - uses: actions/checkout@v4 + + - uses: docker/login-action@0d4c9c5ea7693da7b068278f7b52bda2a190a446 # v3.2.0 + with: + registry: ghcr.io + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + + - uses: elastic/oblt-actions/updatecli/run@v1 + with: + command: --experimental compose diff + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + + - uses: elastic/oblt-actions/updatecli/run@v1 + with: + command: --experimental compose apply + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/NOTICE.txt b/NOTICE.txt index 3cee52c089cb4..bdd6a95e57b04 100644 --- a/NOTICE.txt +++ b/NOTICE.txt @@ -68,68 +68,6 @@ Author Tobias Koppers @sokra --- This product has relied on ASTExplorer that is licensed under MIT. ---- -This product includes code that is based on Ace editor, which was available -under a "BSD" license. - -Distributed under the BSD license: - -Copyright (c) 2010, Ajax.org B.V. -All rights reserved. - - Redistribution and use in source and binary forms, with or without -modification, are permitted provided that the following conditions are met: - * Redistributions of source code must retain the above copyright - notice, this list of conditions and the following disclaimer. - * Redistributions in binary form must reproduce the above copyright - notice, this list of conditions and the following disclaimer in the - documentation and/or other materials provided with the distribution. - * Neither the name of Ajax.org B.V. nor the - names of its contributors may be used to endorse or promote products - derived from this software without specific prior written permission. - - THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND -ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED -WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE -DISCLAIMED. IN NO EVENT SHALL AJAX.ORG B.V. BE LIABLE FOR ANY -DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES -(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; -LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND -ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT -(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS -SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - ---- -This product includes code that is based on Ace editor, which was available -under a "BSD" license. - -Distributed under the BSD license: - -Copyright (c) 2010, Ajax.org B.V. -All rights reserved. - -Redistribution and use in source and binary forms, with or without -modification, are permitted provided that the following conditions are met: - * Redistributions of source code must retain the above copyright - notice, this list of conditions and the following disclaimer. - * Redistributions in binary form must reproduce the above copyright - notice, this list of conditions and the following disclaimer in the - documentation and/or other materials provided with the distribution. - * Neither the name of Ajax.org B.V. nor the - names of its contributors may be used to endorse or promote products - derived from this software without specific prior written permission. - -THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND -ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED -WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE -DISCLAIMED. IN NO EVENT SHALL AJAX.ORG B.V. BE LIABLE FOR ANY -DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES -(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; -LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND -ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT -(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS -SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - --- This product includes code that is based on flot-charts, which was available under a "MIT" license. diff --git a/api_docs/kbn_ace.devdocs.json b/api_docs/kbn_ace.devdocs.json deleted file mode 100644 index 31b9c39264e4d..0000000000000 --- a/api_docs/kbn_ace.devdocs.json +++ /dev/null @@ -1,210 +0,0 @@ -{ - "id": "@kbn/ace", - "client": { - "classes": [], - "functions": [], - "interfaces": [], - "enums": [], - "misc": [], - "objects": [] - }, - "server": { - "classes": [], - "functions": [], - "interfaces": [], - "enums": [], - "misc": [], - "objects": [] - }, - "common": { - "classes": [], - "functions": [ - { - "parentPluginId": "@kbn/ace", - "id": "def-common.addToRules", - "type": "Function", - "tags": [], - "label": "addToRules", - "description": [], - "signature": [ - "(otherRules: any, embedUnder: any) => void" - ], - "path": "packages/kbn-ace/src/ace/modes/lexer_rules/x_json_highlight_rules.ts", - "deprecated": false, - "trackAdoption": false, - "children": [ - { - "parentPluginId": "@kbn/ace", - "id": "def-common.addToRules.$1", - "type": "Any", - "tags": [], - "label": "otherRules", - "description": [], - "signature": [ - "any" - ], - "path": "packages/kbn-ace/src/ace/modes/lexer_rules/x_json_highlight_rules.ts", - "deprecated": false, - "trackAdoption": false, - "isRequired": true - }, - { - "parentPluginId": "@kbn/ace", - "id": "def-common.addToRules.$2", - "type": "Any", - "tags": [], - "label": "embedUnder", - "description": [], - "signature": [ - "any" - ], - "path": "packages/kbn-ace/src/ace/modes/lexer_rules/x_json_highlight_rules.ts", - "deprecated": false, - "trackAdoption": false, - "isRequired": true - } - ], - "returnComment": [], - "initialIsOpen": false - }, - { - "parentPluginId": "@kbn/ace", - "id": "def-common.ElasticsearchSqlHighlightRules", - "type": "Function", - "tags": [], - "label": "ElasticsearchSqlHighlightRules", - "description": [], - "signature": [ - "(this: any) => void" - ], - "path": "packages/kbn-ace/src/ace/modes/lexer_rules/elasticsearch_sql_highlight_rules.ts", - "deprecated": false, - "trackAdoption": false, - "returnComment": [], - "children": [], - "initialIsOpen": false - }, - { - "parentPluginId": "@kbn/ace", - "id": "def-common.installXJsonMode", - "type": "Function", - "tags": [], - "label": "installXJsonMode", - "description": [], - "signature": [ - "(editor: ", - "Editor", - ") => void" - ], - "path": "packages/kbn-ace/src/ace/modes/x_json/x_json.ts", - "deprecated": false, - "trackAdoption": false, - "children": [ - { - "parentPluginId": "@kbn/ace", - "id": "def-common.installXJsonMode.$1", - "type": "Object", - "tags": [], - "label": "editor", - "description": [], - "signature": [ - "Editor" - ], - "path": "packages/kbn-ace/src/ace/modes/x_json/x_json.ts", - "deprecated": false, - "trackAdoption": false, - "isRequired": true - } - ], - "returnComment": [], - "initialIsOpen": false - }, - { - "parentPluginId": "@kbn/ace", - "id": "def-common.ScriptHighlightRules", - "type": "Function", - "tags": [], - "label": "ScriptHighlightRules", - "description": [], - "signature": [ - "(this: any) => void" - ], - "path": "packages/kbn-ace/src/ace/modes/lexer_rules/script_highlight_rules.ts", - "deprecated": false, - "trackAdoption": false, - "children": [ - { - "parentPluginId": "@kbn/ace", - "id": "def-common.ScriptHighlightRules.$1", - "type": "Any", - "tags": [], - "label": "this", - "description": [], - "signature": [ - "any" - ], - "path": "packages/kbn-ace/src/ace/modes/lexer_rules/script_highlight_rules.ts", - "deprecated": false, - "trackAdoption": false, - "isRequired": true - } - ], - "returnComment": [], - "initialIsOpen": false - }, - { - "parentPluginId": "@kbn/ace", - "id": "def-common.XJsonHighlightRules", - "type": "Function", - "tags": [], - "label": "XJsonHighlightRules", - "description": [], - "signature": [ - "(this: any) => void" - ], - "path": "packages/kbn-ace/src/ace/modes/lexer_rules/x_json_highlight_rules.ts", - "deprecated": false, - "trackAdoption": false, - "children": [ - { - "parentPluginId": "@kbn/ace", - "id": "def-common.XJsonHighlightRules.$1", - "type": "Any", - "tags": [], - "label": "this", - "description": [], - "signature": [ - "any" - ], - "path": "packages/kbn-ace/src/ace/modes/lexer_rules/x_json_highlight_rules.ts", - "deprecated": false, - "trackAdoption": false, - "isRequired": true - } - ], - "returnComment": [], - "initialIsOpen": false - } - ], - "interfaces": [], - "enums": [], - "misc": [ - { - "parentPluginId": "@kbn/ace", - "id": "def-common.XJsonMode", - "type": "Any", - "tags": [], - "label": "XJsonMode", - "description": [], - "signature": [ - "any" - ], - "path": "packages/kbn-ace/src/ace/modes/x_json/x_json.ts", - "deprecated": false, - "trackAdoption": false, - "initialIsOpen": false - } - ], - "objects": [] - } -} \ No newline at end of file diff --git a/api_docs/kbn_ace.mdx b/api_docs/kbn_ace.mdx deleted file mode 100644 index 64aba3c6788e8..0000000000000 --- a/api_docs/kbn_ace.mdx +++ /dev/null @@ -1,33 +0,0 @@ ---- -#### -#### This document is auto-generated and is meant to be viewed inside our experimental, new docs system. -#### Reach out in #docs-engineering for more info. -#### -id: kibKbnAcePluginApi -slug: /kibana-dev-docs/api/kbn-ace -title: "@kbn/ace" -image: https://source.unsplash.com/400x175/?github -description: API docs for the @kbn/ace plugin -date: 2024-10-09 -tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/ace'] ---- -import kbnAceObj from './kbn_ace.devdocs.json'; - - - -Contact [@elastic/kibana-management](https://github.com/orgs/elastic/teams/kibana-management) for questions regarding this plugin. - -**Code health stats** - -| Public API count | Any count | Items lacking comments | Missing exports | -|-------------------|-----------|------------------------|-----------------| -| 11 | 5 | 11 | 0 | - -## Common - -### Functions - - -### Consts, variables and types - - diff --git a/api_docs/kbn_elastic_assistant_common.devdocs.json b/api_docs/kbn_elastic_assistant_common.devdocs.json index d06fdf76fcd6e..e7f944dfbcc10 100644 --- a/api_docs/kbn_elastic_assistant_common.devdocs.json +++ b/api_docs/kbn_elastic_assistant_common.devdocs.json @@ -3443,7 +3443,7 @@ "label": "ReadKnowledgeBaseResponse", "description": [], "signature": [ - "{ elser_exists?: boolean | undefined; esql_exists?: boolean | undefined; index_exists?: boolean | undefined; is_setup_available?: boolean | undefined; is_setup_in_progress?: boolean | undefined; pipeline_exists?: boolean | undefined; security_labs_exists?: boolean | undefined; }" + "{ elser_exists?: boolean | undefined; index_exists?: boolean | undefined; is_setup_available?: boolean | undefined; is_setup_in_progress?: boolean | undefined; pipeline_exists?: boolean | undefined; security_labs_exists?: boolean | undefined; }" ], "path": "x-pack/packages/kbn-elastic-assistant-common/impl/schemas/knowledge_base/crud_kb_route.gen.ts", "deprecated": false, @@ -5737,7 +5737,7 @@ "label": "ReadKnowledgeBaseResponse", "description": [], "signature": [ - "Zod.ZodObject<{ elser_exists: Zod.ZodOptional; esql_exists: Zod.ZodOptional; index_exists: Zod.ZodOptional; is_setup_available: Zod.ZodOptional; is_setup_in_progress: Zod.ZodOptional; pipeline_exists: Zod.ZodOptional; security_labs_exists: Zod.ZodOptional; }, \"strip\", Zod.ZodTypeAny, { elser_exists?: boolean | undefined; esql_exists?: boolean | undefined; index_exists?: boolean | undefined; is_setup_available?: boolean | undefined; is_setup_in_progress?: boolean | undefined; pipeline_exists?: boolean | undefined; security_labs_exists?: boolean | undefined; }, { elser_exists?: boolean | undefined; esql_exists?: boolean | undefined; index_exists?: boolean | undefined; is_setup_available?: boolean | undefined; is_setup_in_progress?: boolean | undefined; pipeline_exists?: boolean | undefined; security_labs_exists?: boolean | undefined; }>" + "Zod.ZodObject<{ elser_exists: Zod.ZodOptional; index_exists: Zod.ZodOptional; is_setup_available: Zod.ZodOptional; is_setup_in_progress: Zod.ZodOptional; pipeline_exists: Zod.ZodOptional; security_labs_exists: Zod.ZodOptional; }, \"strip\", Zod.ZodTypeAny, { elser_exists?: boolean | undefined; esql_exists?: boolean | undefined; index_exists?: boolean | undefined; is_setup_available?: boolean | undefined; is_setup_in_progress?: boolean | undefined; pipeline_exists?: boolean | undefined; security_labs_exists?: boolean | undefined; }, { elser_exists?: boolean | undefined; esql_exists?: boolean | undefined; index_exists?: boolean | undefined; is_setup_available?: boolean | undefined; is_setup_in_progress?: boolean | undefined; pipeline_exists?: boolean | undefined; security_labs_exists?: boolean | undefined; }>" ], "path": "x-pack/packages/kbn-elastic-assistant-common/impl/schemas/knowledge_base/crud_kb_route.gen.ts", "deprecated": false, diff --git a/api_docs/plugin_directory.mdx b/api_docs/plugin_directory.mdx index a5a2307c4d6db..959b02632bf07 100644 --- a/api_docs/plugin_directory.mdx +++ b/api_docs/plugin_directory.mdx @@ -242,7 +242,6 @@ tags: ['contributor', 'dev', 'apidocs', 'kibana'] | Package name           | Maintaining team | Description | API Cnt | Any Cnt | Missing
comments | Missing
exports | |--------------|----------------|-----------|--------------|----------|---------------|--------| -| | [@elastic/kibana-management](https://github.com/orgs/elastic/teams/kibana-management) | - | 11 | 5 | 11 | 0 | | | [@elastic/response-ops](https://github.com/orgs/elastic/teams/response-ops) | - | 14 | 0 | 14 | 0 | | | [@elastic/ml-ui](https://github.com/orgs/elastic/teams/ml-ui) | - | 36 | 0 | 0 | 0 | | | [@elastic/ml-ui](https://github.com/orgs/elastic/teams/ml-ui) | - | 2 | 0 | 0 | 0 | @@ -797,4 +796,3 @@ tags: ['contributor', 'dev', 'apidocs', 'kibana'] | | [@elastic/kibana-operations](https://github.com/orgs/elastic/teams/kibana-operations) | - | 9 | 0 | 4 | 0 | | | [@elastic/kibana-core](https://github.com/orgs/elastic/teams/kibana-core) | - | 1254 | 0 | 4 | 0 | | | [@elastic/security-detection-rule-management](https://github.com/orgs/elastic/teams/security-detection-rule-management) | - | 20 | 0 | 10 | 0 | - diff --git a/api_docs/security_solution.devdocs.json b/api_docs/security_solution.devdocs.json index a9bcc310b662d..9a94122c32223 100644 --- a/api_docs/security_solution.devdocs.json +++ b/api_docs/security_solution.devdocs.json @@ -420,7 +420,7 @@ "\nExperimental flag needed to enable the link" ], "signature": [ - "\"assistantKnowledgeBaseByDefault\" | \"assistantModelEvaluation\" | \"excludePoliciesInFilterEnabled\" | \"kubernetesEnabled\" | \"donutChartEmbeddablesEnabled\" | \"previewTelemetryUrlEnabled\" | \"extendedRuleExecutionLoggingEnabled\" | \"socTrendsEnabled\" | \"responseActionUploadEnabled\" | \"automatedProcessActionsEnabled\" | \"responseActionsSentinelOneV1Enabled\" | \"responseActionsSentinelOneV2Enabled\" | \"responseActionsSentinelOneGetFileEnabled\" | \"responseActionsSentinelOneKillProcessEnabled\" | \"responseActionsSentinelOneProcessesEnabled\" | \"responseActionsCrowdstrikeManualHostIsolationEnabled\" | \"endpointManagementSpaceAwarenessEnabled\" | \"securitySolutionNotesEnabled\" | \"entityAlertPreviewDisabled\" | \"assistantNaturalLanguageESQLTool\" | \"newUserDetailsFlyoutManagedUser\" | \"riskScoringPersistence\" | \"riskScoringRoutesEnabled\" | \"esqlRulesDisabled\" | \"loggingRequestsEnabled\" | \"protectionUpdatesEnabled\" | \"disableTimelineSaveTour\" | \"riskEnginePrivilegesRouteEnabled\" | \"sentinelOneDataInAnalyzerEnabled\" | \"sentinelOneManualHostActionsEnabled\" | \"crowdstrikeDataInAnalyzerEnabled\" | \"responseActionsTelemetryEnabled\" | \"jamfDataInAnalyzerEnabled\" | \"timelineEsqlTabDisabled\" | \"unifiedComponentsInTimelineDisabled\" | \"analyzerDatePickersAndSourcererDisabled\" | \"prebuiltRulesCustomizationEnabled\" | \"malwareOnWriteScanOptionAvailable\" | \"unifiedManifestEnabled\" | \"valueListItemsModalEnabled\" | \"manualRuleRunEnabled\" | \"filterProcessDescendantsForEventFiltersEnabled\" | \"dataIngestionHubEnabled\" | \"entityStoreEnabled\" | undefined" + "\"assistantKnowledgeBaseByDefault\" | \"assistantModelEvaluation\" | \"excludePoliciesInFilterEnabled\" | \"kubernetesEnabled\" | \"donutChartEmbeddablesEnabled\" | \"previewTelemetryUrlEnabled\" | \"extendedRuleExecutionLoggingEnabled\" | \"socTrendsEnabled\" | \"responseActionUploadEnabled\" | \"automatedProcessActionsEnabled\" | \"responseActionsSentinelOneV1Enabled\" | \"responseActionsSentinelOneV2Enabled\" | \"responseActionsSentinelOneGetFileEnabled\" | \"responseActionsSentinelOneKillProcessEnabled\" | \"responseActionsSentinelOneProcessesEnabled\" | \"responseActionsCrowdstrikeManualHostIsolationEnabled\" | \"endpointManagementSpaceAwarenessEnabled\" | \"securitySolutionNotesEnabled\" | \"entityAlertPreviewDisabled\" | \"newUserDetailsFlyoutManagedUser\" | \"riskScoringPersistence\" | \"riskScoringRoutesEnabled\" | \"esqlRulesDisabled\" | \"loggingRequestsEnabled\" | \"protectionUpdatesEnabled\" | \"disableTimelineSaveTour\" | \"riskEnginePrivilegesRouteEnabled\" | \"sentinelOneDataInAnalyzerEnabled\" | \"sentinelOneManualHostActionsEnabled\" | \"crowdstrikeDataInAnalyzerEnabled\" | \"responseActionsTelemetryEnabled\" | \"jamfDataInAnalyzerEnabled\" | \"timelineEsqlTabDisabled\" | \"unifiedComponentsInTimelineDisabled\" | \"analyzerDatePickersAndSourcererDisabled\" | \"prebuiltRulesCustomizationEnabled\" | \"malwareOnWriteScanOptionAvailable\" | \"unifiedManifestEnabled\" | \"valueListItemsModalEnabled\" | \"manualRuleRunEnabled\" | \"filterProcessDescendantsForEventFiltersEnabled\" | \"dataIngestionHubEnabled\" | \"entityStoreEnabled\" | undefined" ], "path": "x-pack/plugins/security_solution/public/common/links/types.ts", "deprecated": false, @@ -500,7 +500,7 @@ "\nExperimental flag needed to disable the link. Opposite of experimentalKey" ], "signature": [ - "\"assistantKnowledgeBaseByDefault\" | \"assistantModelEvaluation\" | \"excludePoliciesInFilterEnabled\" | \"kubernetesEnabled\" | \"donutChartEmbeddablesEnabled\" | \"previewTelemetryUrlEnabled\" | \"extendedRuleExecutionLoggingEnabled\" | \"socTrendsEnabled\" | \"responseActionUploadEnabled\" | \"automatedProcessActionsEnabled\" | \"responseActionsSentinelOneV1Enabled\" | \"responseActionsSentinelOneV2Enabled\" | \"responseActionsSentinelOneGetFileEnabled\" | \"responseActionsSentinelOneKillProcessEnabled\" | \"responseActionsSentinelOneProcessesEnabled\" | \"responseActionsCrowdstrikeManualHostIsolationEnabled\" | \"endpointManagementSpaceAwarenessEnabled\" | \"securitySolutionNotesEnabled\" | \"entityAlertPreviewDisabled\" | \"assistantNaturalLanguageESQLTool\" | \"newUserDetailsFlyoutManagedUser\" | \"riskScoringPersistence\" | \"riskScoringRoutesEnabled\" | \"esqlRulesDisabled\" | \"loggingRequestsEnabled\" | \"protectionUpdatesEnabled\" | \"disableTimelineSaveTour\" | \"riskEnginePrivilegesRouteEnabled\" | \"sentinelOneDataInAnalyzerEnabled\" | \"sentinelOneManualHostActionsEnabled\" | \"crowdstrikeDataInAnalyzerEnabled\" | \"responseActionsTelemetryEnabled\" | \"jamfDataInAnalyzerEnabled\" | \"timelineEsqlTabDisabled\" | \"unifiedComponentsInTimelineDisabled\" | \"analyzerDatePickersAndSourcererDisabled\" | \"prebuiltRulesCustomizationEnabled\" | \"malwareOnWriteScanOptionAvailable\" | \"unifiedManifestEnabled\" | \"valueListItemsModalEnabled\" | \"manualRuleRunEnabled\" | \"filterProcessDescendantsForEventFiltersEnabled\" | \"dataIngestionHubEnabled\" | \"entityStoreEnabled\" | undefined" + "\"assistantKnowledgeBaseByDefault\" | \"assistantModelEvaluation\" | \"excludePoliciesInFilterEnabled\" | \"kubernetesEnabled\" | \"donutChartEmbeddablesEnabled\" | \"previewTelemetryUrlEnabled\" | \"extendedRuleExecutionLoggingEnabled\" | \"socTrendsEnabled\" | \"responseActionUploadEnabled\" | \"automatedProcessActionsEnabled\" | \"responseActionsSentinelOneV1Enabled\" | \"responseActionsSentinelOneV2Enabled\" | \"responseActionsSentinelOneGetFileEnabled\" | \"responseActionsSentinelOneKillProcessEnabled\" | \"responseActionsSentinelOneProcessesEnabled\" | \"responseActionsCrowdstrikeManualHostIsolationEnabled\" | \"endpointManagementSpaceAwarenessEnabled\" | \"securitySolutionNotesEnabled\" | \"entityAlertPreviewDisabled\" | \"newUserDetailsFlyoutManagedUser\" | \"riskScoringPersistence\" | \"riskScoringRoutesEnabled\" | \"esqlRulesDisabled\" | \"loggingRequestsEnabled\" | \"protectionUpdatesEnabled\" | \"disableTimelineSaveTour\" | \"riskEnginePrivilegesRouteEnabled\" | \"sentinelOneDataInAnalyzerEnabled\" | \"sentinelOneManualHostActionsEnabled\" | \"crowdstrikeDataInAnalyzerEnabled\" | \"responseActionsTelemetryEnabled\" | \"jamfDataInAnalyzerEnabled\" | \"timelineEsqlTabDisabled\" | \"unifiedComponentsInTimelineDisabled\" | \"analyzerDatePickersAndSourcererDisabled\" | \"prebuiltRulesCustomizationEnabled\" | \"malwareOnWriteScanOptionAvailable\" | \"unifiedManifestEnabled\" | \"valueListItemsModalEnabled\" | \"manualRuleRunEnabled\" | \"filterProcessDescendantsForEventFiltersEnabled\" | \"dataIngestionHubEnabled\" | \"entityStoreEnabled\" | undefined" ], "path": "x-pack/plugins/security_solution/public/common/links/types.ts", "deprecated": false, @@ -1864,7 +1864,7 @@ "label": "experimentalFeatures", "description": [], "signature": [ - "{ readonly excludePoliciesInFilterEnabled: boolean; readonly kubernetesEnabled: boolean; readonly donutChartEmbeddablesEnabled: boolean; readonly previewTelemetryUrlEnabled: boolean; readonly extendedRuleExecutionLoggingEnabled: boolean; readonly socTrendsEnabled: boolean; readonly responseActionUploadEnabled: boolean; readonly automatedProcessActionsEnabled: boolean; readonly responseActionsSentinelOneV1Enabled: boolean; readonly responseActionsSentinelOneV2Enabled: boolean; readonly responseActionsSentinelOneGetFileEnabled: boolean; readonly responseActionsSentinelOneKillProcessEnabled: boolean; readonly responseActionsSentinelOneProcessesEnabled: boolean; readonly responseActionsCrowdstrikeManualHostIsolationEnabled: boolean; readonly endpointManagementSpaceAwarenessEnabled: boolean; readonly securitySolutionNotesEnabled: boolean; readonly entityAlertPreviewDisabled: boolean; readonly assistantModelEvaluation: boolean; readonly assistantKnowledgeBaseByDefault: boolean; readonly assistantNaturalLanguageESQLTool: boolean; readonly newUserDetailsFlyoutManagedUser: boolean; readonly riskScoringPersistence: boolean; readonly riskScoringRoutesEnabled: boolean; readonly esqlRulesDisabled: boolean; readonly loggingRequestsEnabled: boolean; readonly protectionUpdatesEnabled: boolean; readonly disableTimelineSaveTour: boolean; readonly riskEnginePrivilegesRouteEnabled: boolean; readonly sentinelOneDataInAnalyzerEnabled: boolean; readonly sentinelOneManualHostActionsEnabled: boolean; readonly crowdstrikeDataInAnalyzerEnabled: boolean; readonly responseActionsTelemetryEnabled: boolean; readonly jamfDataInAnalyzerEnabled: boolean; readonly timelineEsqlTabDisabled: boolean; readonly unifiedComponentsInTimelineDisabled: boolean; readonly analyzerDatePickersAndSourcererDisabled: boolean; readonly prebuiltRulesCustomizationEnabled: boolean; readonly malwareOnWriteScanOptionAvailable: boolean; readonly unifiedManifestEnabled: boolean; readonly valueListItemsModalEnabled: boolean; readonly manualRuleRunEnabled: boolean; readonly filterProcessDescendantsForEventFiltersEnabled: boolean; readonly dataIngestionHubEnabled: boolean; readonly entityStoreEnabled: boolean; }" + "{ readonly excludePoliciesInFilterEnabled: boolean; readonly kubernetesEnabled: boolean; readonly donutChartEmbeddablesEnabled: boolean; readonly previewTelemetryUrlEnabled: boolean; readonly extendedRuleExecutionLoggingEnabled: boolean; readonly socTrendsEnabled: boolean; readonly responseActionUploadEnabled: boolean; readonly automatedProcessActionsEnabled: boolean; readonly responseActionsSentinelOneV1Enabled: boolean; readonly responseActionsSentinelOneV2Enabled: boolean; readonly responseActionsSentinelOneGetFileEnabled: boolean; readonly responseActionsSentinelOneKillProcessEnabled: boolean; readonly responseActionsSentinelOneProcessesEnabled: boolean; readonly responseActionsCrowdstrikeManualHostIsolationEnabled: boolean; readonly endpointManagementSpaceAwarenessEnabled: boolean; readonly securitySolutionNotesEnabled: boolean; readonly entityAlertPreviewDisabled: boolean; readonly assistantModelEvaluation: boolean; readonly assistantKnowledgeBaseByDefault: boolean; readonly newUserDetailsFlyoutManagedUser: boolean; readonly riskScoringPersistence: boolean; readonly riskScoringRoutesEnabled: boolean; readonly esqlRulesDisabled: boolean; readonly loggingRequestsEnabled: boolean; readonly protectionUpdatesEnabled: boolean; readonly disableTimelineSaveTour: boolean; readonly riskEnginePrivilegesRouteEnabled: boolean; readonly sentinelOneDataInAnalyzerEnabled: boolean; readonly sentinelOneManualHostActionsEnabled: boolean; readonly crowdstrikeDataInAnalyzerEnabled: boolean; readonly responseActionsTelemetryEnabled: boolean; readonly jamfDataInAnalyzerEnabled: boolean; readonly timelineEsqlTabDisabled: boolean; readonly unifiedComponentsInTimelineDisabled: boolean; readonly analyzerDatePickersAndSourcererDisabled: boolean; readonly prebuiltRulesCustomizationEnabled: boolean; readonly malwareOnWriteScanOptionAvailable: boolean; readonly unifiedManifestEnabled: boolean; readonly valueListItemsModalEnabled: boolean; readonly manualRuleRunEnabled: boolean; readonly filterProcessDescendantsForEventFiltersEnabled: boolean; readonly dataIngestionHubEnabled: boolean; readonly entityStoreEnabled: boolean; }" ], "path": "x-pack/plugins/security_solution/public/types.ts", "deprecated": false, @@ -3032,7 +3032,7 @@ "\nThe security solution generic experimental features" ], "signature": [ - "{ readonly excludePoliciesInFilterEnabled: boolean; readonly kubernetesEnabled: boolean; readonly donutChartEmbeddablesEnabled: boolean; readonly previewTelemetryUrlEnabled: boolean; readonly extendedRuleExecutionLoggingEnabled: boolean; readonly socTrendsEnabled: boolean; readonly responseActionUploadEnabled: boolean; readonly automatedProcessActionsEnabled: boolean; readonly responseActionsSentinelOneV1Enabled: boolean; readonly responseActionsSentinelOneV2Enabled: boolean; readonly responseActionsSentinelOneGetFileEnabled: boolean; readonly responseActionsSentinelOneKillProcessEnabled: boolean; readonly responseActionsSentinelOneProcessesEnabled: boolean; readonly responseActionsCrowdstrikeManualHostIsolationEnabled: boolean; readonly endpointManagementSpaceAwarenessEnabled: boolean; readonly securitySolutionNotesEnabled: boolean; readonly entityAlertPreviewDisabled: boolean; readonly assistantModelEvaluation: boolean; readonly assistantKnowledgeBaseByDefault: boolean; readonly assistantNaturalLanguageESQLTool: boolean; readonly newUserDetailsFlyoutManagedUser: boolean; readonly riskScoringPersistence: boolean; readonly riskScoringRoutesEnabled: boolean; readonly esqlRulesDisabled: boolean; readonly loggingRequestsEnabled: boolean; readonly protectionUpdatesEnabled: boolean; readonly disableTimelineSaveTour: boolean; readonly riskEnginePrivilegesRouteEnabled: boolean; readonly sentinelOneDataInAnalyzerEnabled: boolean; readonly sentinelOneManualHostActionsEnabled: boolean; readonly crowdstrikeDataInAnalyzerEnabled: boolean; readonly responseActionsTelemetryEnabled: boolean; readonly jamfDataInAnalyzerEnabled: boolean; readonly timelineEsqlTabDisabled: boolean; readonly unifiedComponentsInTimelineDisabled: boolean; readonly analyzerDatePickersAndSourcererDisabled: boolean; readonly prebuiltRulesCustomizationEnabled: boolean; readonly malwareOnWriteScanOptionAvailable: boolean; readonly unifiedManifestEnabled: boolean; readonly valueListItemsModalEnabled: boolean; readonly manualRuleRunEnabled: boolean; readonly filterProcessDescendantsForEventFiltersEnabled: boolean; readonly dataIngestionHubEnabled: boolean; readonly entityStoreEnabled: boolean; }" + "{ readonly excludePoliciesInFilterEnabled: boolean; readonly kubernetesEnabled: boolean; readonly donutChartEmbeddablesEnabled: boolean; readonly previewTelemetryUrlEnabled: boolean; readonly extendedRuleExecutionLoggingEnabled: boolean; readonly socTrendsEnabled: boolean; readonly responseActionUploadEnabled: boolean; readonly automatedProcessActionsEnabled: boolean; readonly responseActionsSentinelOneV1Enabled: boolean; readonly responseActionsSentinelOneV2Enabled: boolean; readonly responseActionsSentinelOneGetFileEnabled: boolean; readonly responseActionsSentinelOneKillProcessEnabled: boolean; readonly responseActionsSentinelOneProcessesEnabled: boolean; readonly responseActionsCrowdstrikeManualHostIsolationEnabled: boolean; readonly endpointManagementSpaceAwarenessEnabled: boolean; readonly securitySolutionNotesEnabled: boolean; readonly entityAlertPreviewDisabled: boolean; readonly assistantModelEvaluation: boolean; readonly assistantKnowledgeBaseByDefault: boolean; readonly newUserDetailsFlyoutManagedUser: boolean; readonly riskScoringPersistence: boolean; readonly riskScoringRoutesEnabled: boolean; readonly esqlRulesDisabled: boolean; readonly loggingRequestsEnabled: boolean; readonly protectionUpdatesEnabled: boolean; readonly disableTimelineSaveTour: boolean; readonly riskEnginePrivilegesRouteEnabled: boolean; readonly sentinelOneDataInAnalyzerEnabled: boolean; readonly sentinelOneManualHostActionsEnabled: boolean; readonly crowdstrikeDataInAnalyzerEnabled: boolean; readonly responseActionsTelemetryEnabled: boolean; readonly jamfDataInAnalyzerEnabled: boolean; readonly timelineEsqlTabDisabled: boolean; readonly unifiedComponentsInTimelineDisabled: boolean; readonly analyzerDatePickersAndSourcererDisabled: boolean; readonly prebuiltRulesCustomizationEnabled: boolean; readonly malwareOnWriteScanOptionAvailable: boolean; readonly unifiedManifestEnabled: boolean; readonly valueListItemsModalEnabled: boolean; readonly manualRuleRunEnabled: boolean; readonly filterProcessDescendantsForEventFiltersEnabled: boolean; readonly dataIngestionHubEnabled: boolean; readonly entityStoreEnabled: boolean; }" ], "path": "x-pack/plugins/security_solution/server/plugin_contract.ts", "deprecated": false, @@ -3208,7 +3208,7 @@ "label": "ExperimentalFeatures", "description": [], "signature": [ - "{ readonly excludePoliciesInFilterEnabled: boolean; readonly kubernetesEnabled: boolean; readonly donutChartEmbeddablesEnabled: boolean; readonly previewTelemetryUrlEnabled: boolean; readonly extendedRuleExecutionLoggingEnabled: boolean; readonly socTrendsEnabled: boolean; readonly responseActionUploadEnabled: boolean; readonly automatedProcessActionsEnabled: boolean; readonly responseActionsSentinelOneV1Enabled: boolean; readonly responseActionsSentinelOneV2Enabled: boolean; readonly responseActionsSentinelOneGetFileEnabled: boolean; readonly responseActionsSentinelOneKillProcessEnabled: boolean; readonly responseActionsSentinelOneProcessesEnabled: boolean; readonly responseActionsCrowdstrikeManualHostIsolationEnabled: boolean; readonly endpointManagementSpaceAwarenessEnabled: boolean; readonly securitySolutionNotesEnabled: boolean; readonly entityAlertPreviewDisabled: boolean; readonly assistantModelEvaluation: boolean; readonly assistantKnowledgeBaseByDefault: boolean; readonly assistantNaturalLanguageESQLTool: boolean; readonly newUserDetailsFlyoutManagedUser: boolean; readonly riskScoringPersistence: boolean; readonly riskScoringRoutesEnabled: boolean; readonly esqlRulesDisabled: boolean; readonly loggingRequestsEnabled: boolean; readonly protectionUpdatesEnabled: boolean; readonly disableTimelineSaveTour: boolean; readonly riskEnginePrivilegesRouteEnabled: boolean; readonly sentinelOneDataInAnalyzerEnabled: boolean; readonly sentinelOneManualHostActionsEnabled: boolean; readonly crowdstrikeDataInAnalyzerEnabled: boolean; readonly responseActionsTelemetryEnabled: boolean; readonly jamfDataInAnalyzerEnabled: boolean; readonly timelineEsqlTabDisabled: boolean; readonly unifiedComponentsInTimelineDisabled: boolean; readonly analyzerDatePickersAndSourcererDisabled: boolean; readonly prebuiltRulesCustomizationEnabled: boolean; readonly malwareOnWriteScanOptionAvailable: boolean; readonly unifiedManifestEnabled: boolean; readonly valueListItemsModalEnabled: boolean; readonly manualRuleRunEnabled: boolean; readonly filterProcessDescendantsForEventFiltersEnabled: boolean; readonly dataIngestionHubEnabled: boolean; readonly entityStoreEnabled: boolean; }" + "{ readonly excludePoliciesInFilterEnabled: boolean; readonly kubernetesEnabled: boolean; readonly donutChartEmbeddablesEnabled: boolean; readonly previewTelemetryUrlEnabled: boolean; readonly extendedRuleExecutionLoggingEnabled: boolean; readonly socTrendsEnabled: boolean; readonly responseActionUploadEnabled: boolean; readonly automatedProcessActionsEnabled: boolean; readonly responseActionsSentinelOneV1Enabled: boolean; readonly responseActionsSentinelOneV2Enabled: boolean; readonly responseActionsSentinelOneGetFileEnabled: boolean; readonly responseActionsSentinelOneKillProcessEnabled: boolean; readonly responseActionsSentinelOneProcessesEnabled: boolean; readonly responseActionsCrowdstrikeManualHostIsolationEnabled: boolean; readonly endpointManagementSpaceAwarenessEnabled: boolean; readonly securitySolutionNotesEnabled: boolean; readonly entityAlertPreviewDisabled: boolean; readonly assistantModelEvaluation: boolean; readonly assistantKnowledgeBaseByDefault: boolean; readonly newUserDetailsFlyoutManagedUser: boolean; readonly riskScoringPersistence: boolean; readonly riskScoringRoutesEnabled: boolean; readonly esqlRulesDisabled: boolean; readonly loggingRequestsEnabled: boolean; readonly protectionUpdatesEnabled: boolean; readonly disableTimelineSaveTour: boolean; readonly riskEnginePrivilegesRouteEnabled: boolean; readonly sentinelOneDataInAnalyzerEnabled: boolean; readonly sentinelOneManualHostActionsEnabled: boolean; readonly crowdstrikeDataInAnalyzerEnabled: boolean; readonly responseActionsTelemetryEnabled: boolean; readonly jamfDataInAnalyzerEnabled: boolean; readonly timelineEsqlTabDisabled: boolean; readonly unifiedComponentsInTimelineDisabled: boolean; readonly analyzerDatePickersAndSourcererDisabled: boolean; readonly prebuiltRulesCustomizationEnabled: boolean; readonly malwareOnWriteScanOptionAvailable: boolean; readonly unifiedManifestEnabled: boolean; readonly valueListItemsModalEnabled: boolean; readonly manualRuleRunEnabled: boolean; readonly filterProcessDescendantsForEventFiltersEnabled: boolean; readonly dataIngestionHubEnabled: boolean; readonly entityStoreEnabled: boolean; }" ], "path": "x-pack/plugins/security_solution/common/experimental_features.ts", "deprecated": false, @@ -3274,7 +3274,7 @@ "\nA list of allowed values that can be used in `xpack.securitySolution.enableExperimental`.\nThis object is then used to validate and parse the value entered." ], "signature": [ - "{ readonly excludePoliciesInFilterEnabled: false; readonly kubernetesEnabled: true; readonly donutChartEmbeddablesEnabled: false; readonly previewTelemetryUrlEnabled: false; readonly extendedRuleExecutionLoggingEnabled: false; readonly socTrendsEnabled: false; readonly responseActionUploadEnabled: true; readonly automatedProcessActionsEnabled: true; readonly responseActionsSentinelOneV1Enabled: true; readonly responseActionsSentinelOneV2Enabled: true; readonly responseActionsSentinelOneGetFileEnabled: true; readonly responseActionsSentinelOneKillProcessEnabled: true; readonly responseActionsSentinelOneProcessesEnabled: true; readonly responseActionsCrowdstrikeManualHostIsolationEnabled: true; readonly endpointManagementSpaceAwarenessEnabled: false; readonly securitySolutionNotesEnabled: false; readonly entityAlertPreviewDisabled: false; readonly assistantModelEvaluation: false; readonly assistantKnowledgeBaseByDefault: false; readonly assistantNaturalLanguageESQLTool: false; readonly newUserDetailsFlyoutManagedUser: false; readonly riskScoringPersistence: true; readonly riskScoringRoutesEnabled: true; readonly esqlRulesDisabled: false; readonly loggingRequestsEnabled: false; readonly protectionUpdatesEnabled: true; readonly disableTimelineSaveTour: false; readonly riskEnginePrivilegesRouteEnabled: true; readonly sentinelOneDataInAnalyzerEnabled: true; readonly sentinelOneManualHostActionsEnabled: true; readonly crowdstrikeDataInAnalyzerEnabled: true; readonly responseActionsTelemetryEnabled: false; readonly jamfDataInAnalyzerEnabled: true; readonly timelineEsqlTabDisabled: false; readonly unifiedComponentsInTimelineDisabled: false; readonly analyzerDatePickersAndSourcererDisabled: false; readonly prebuiltRulesCustomizationEnabled: false; readonly malwareOnWriteScanOptionAvailable: true; readonly unifiedManifestEnabled: true; readonly valueListItemsModalEnabled: true; readonly manualRuleRunEnabled: false; readonly filterProcessDescendantsForEventFiltersEnabled: true; readonly dataIngestionHubEnabled: false; readonly entityStoreEnabled: false; }" + "{ readonly excludePoliciesInFilterEnabled: false; readonly kubernetesEnabled: true; readonly donutChartEmbeddablesEnabled: false; readonly previewTelemetryUrlEnabled: false; readonly extendedRuleExecutionLoggingEnabled: false; readonly socTrendsEnabled: false; readonly responseActionUploadEnabled: true; readonly automatedProcessActionsEnabled: true; readonly responseActionsSentinelOneV1Enabled: true; readonly responseActionsSentinelOneV2Enabled: true; readonly responseActionsSentinelOneGetFileEnabled: true; readonly responseActionsSentinelOneKillProcessEnabled: true; readonly responseActionsSentinelOneProcessesEnabled: true; readonly responseActionsCrowdstrikeManualHostIsolationEnabled: true; readonly endpointManagementSpaceAwarenessEnabled: false; readonly securitySolutionNotesEnabled: false; readonly entityAlertPreviewDisabled: false; readonly assistantModelEvaluation: false; readonly assistantKnowledgeBaseByDefault: false; readonly newUserDetailsFlyoutManagedUser: false; readonly riskScoringPersistence: true; readonly riskScoringRoutesEnabled: true; readonly esqlRulesDisabled: false; readonly loggingRequestsEnabled: false; readonly protectionUpdatesEnabled: true; readonly disableTimelineSaveTour: false; readonly riskEnginePrivilegesRouteEnabled: true; readonly sentinelOneDataInAnalyzerEnabled: true; readonly sentinelOneManualHostActionsEnabled: true; readonly crowdstrikeDataInAnalyzerEnabled: true; readonly responseActionsTelemetryEnabled: false; readonly jamfDataInAnalyzerEnabled: true; readonly timelineEsqlTabDisabled: false; readonly unifiedComponentsInTimelineDisabled: false; readonly analyzerDatePickersAndSourcererDisabled: false; readonly prebuiltRulesCustomizationEnabled: false; readonly malwareOnWriteScanOptionAvailable: true; readonly unifiedManifestEnabled: true; readonly valueListItemsModalEnabled: true; readonly manualRuleRunEnabled: false; readonly filterProcessDescendantsForEventFiltersEnabled: true; readonly dataIngestionHubEnabled: false; readonly entityStoreEnabled: false; }" ], "path": "x-pack/plugins/security_solution/common/experimental_features.ts", "deprecated": false, diff --git a/dev_docs/nav-kibana-dev.docnav.json b/dev_docs/nav-kibana-dev.docnav.json index 8b8cd64a44664..a7d696fc10574 100644 --- a/dev_docs/nav-kibana-dev.docnav.json +++ b/dev_docs/nav-kibana-dev.docnav.json @@ -278,6 +278,10 @@ { "id": "kibDevReactKibanaContext", "label": "Kibana React Contexts" + }, + { + "id": "kibDevDocsChromeRecentlyAccessed", + "label": "Recently Viewed" } ] }, diff --git a/dev_docs/shared_ux/chrome_recently_accessed/chrome_recently_accessed.mdx b/dev_docs/shared_ux/chrome_recently_accessed/chrome_recently_accessed.mdx new file mode 100644 index 0000000000000..cca466bcf1ac3 --- /dev/null +++ b/dev_docs/shared_ux/chrome_recently_accessed/chrome_recently_accessed.mdx @@ -0,0 +1,66 @@ +--- +id: kibDevDocsChromeRecentlyAccessed +slug: /kibana-dev-docs/chrome/recently-accessed +title: Chrome Recently Viewed +description: How to use chrome's recently accessed service to add your links to the recently viewed list in the side navigation. +date: 2024-10-04 +tags: ['kibana', 'dev', 'contributor', 'chrome', 'navigation', 'shared-ux'] +--- + +## Introduction + +The service allows applications to register recently visited objects. These items are displayed in the "Recently Viewed" section of a side navigation menu, providing users with quick access to their previously visited resources. This service includes methods for adding, retrieving, and subscribing to the recently accessed history. + +![Recently viewed section in the sidenav](./chrome_recently_accessed.png) + +## Guidelines + +The service should be used thoughtfully to provide users with easy access to key resources they've interacted with. Unlike browser history, this feature is for important items that users may want to revisit. + +### DOs + +- Register important resources that users may want to revisit. Like a dashboard, a saved search, or another specific object. +- Update the link when the state of the current resource changes. For example, if a user changes the time range while on a dashboard, update the recently viewed link to reflect the latest viewed state where possible. See below for instructions on how to update the link when state changes. + +### DON'Ts + +- Don't register every page view. +- Don't register temporary or transient states as individual items. +- Prevent overloading. Keep the list focused on high-value resources. +- Don't add a recently viewed object without first speaking to relevant Product Managers. + +## Usage + +To register an item with the `ChromeRecentlyAccessed` service, provide a unique `id`, a `label`, and a `link`. The `id` is used to identify and deduplicate the item, the `label` is displayed in the "Recently Viewed" list and the `link` is used to navigate to the item when selected. + +```ts +const link = '/app/map/1234'; +const label = 'Map 1234'; +const id = 'map-1234'; + +coreStart.chrome.recentlyAccessed.add(link, label, id); +``` + +To update the link when state changes, add another item with the same `id`. This will replace the existing item in the "Recently Viewed" list. + +```ts +const link = '/app/map/1234'; +const label = 'Map 1234'; + +coreStart.chrome.recentlyAccessed.add(`/app/map/1234`, label, id); + +// User changes the time range and we want to update the link in the "Recently Viewed" list +coreStart.chrome.recentlyAccessed.add( + `/app/map/1234?timeRangeFrom=now-30m&timeRangeTo=now`, + label, + id +); +``` + +## Implementation details + +The services is based on package. This package provides a `RecentlyAccessedService` that uses browser local storage to manage records of recently accessed objects. Internally it implements the queue with a maximum length of 20 items. When the queue is full, the oldest item is removed. +Applications can create their own instance of `RecentlyAccessedService` to manage their own list of recently accessed items scoped to their application. + +- is a service available via `coreStart.chrome.recentlyAccessed` and should be used to add items to chrome's sidenav. +- is package that `ChromeRecentlyAccessed` is using internally and the package can be used to create your own instance and manage your own list of recently accessed items that is independent for chrome's sidenav. \ No newline at end of file diff --git a/dev_docs/shared_ux/chrome_recently_accessed/chrome_recently_accessed.png b/dev_docs/shared_ux/chrome_recently_accessed/chrome_recently_accessed.png new file mode 100644 index 0000000000000..41d3913b048a2 Binary files /dev/null and b/dev_docs/shared_ux/chrome_recently_accessed/chrome_recently_accessed.png differ diff --git a/dev_docs/shared_ux/shared_ux_landing.mdx b/dev_docs/shared_ux/shared_ux_landing.mdx index 4be8ad134be15..d96798eefa61f 100644 --- a/dev_docs/shared_ux/shared_ux_landing.mdx +++ b/dev_docs/shared_ux/shared_ux_landing.mdx @@ -66,5 +66,10 @@ layout: landing title: 'Kibana React Contexts', description: 'Learn how to use common React contexts in Kibana', }, + { + pageId: 'kibDevDocsChromeRecentlyAccessed', + title: 'Chrome Recently Viewed', + description: 'Learn how to add recently viewed items to the side navigation', + }, ]} /> diff --git a/docs/developer/getting-started/monorepo-packages.asciidoc b/docs/developer/getting-started/monorepo-packages.asciidoc index 50095f8b7018f..0b97a425001ec 100644 --- a/docs/developer/getting-started/monorepo-packages.asciidoc +++ b/docs/developer/getting-started/monorepo-packages.asciidoc @@ -41,7 +41,6 @@ yarn kbn watch [discrete] === List of Already Migrated Packages to Bazel -- @kbn/ace - @kbn/analytics - @kbn/apm-config-loader - @kbn/apm-utils @@ -93,4 +92,4 @@ yarn kbn watch - @kbn/ui-shared-deps-npm - @kbn/ui-shared-deps-src - @kbn/utility-types -- @kbn/utils +- @kbn/utils \ No newline at end of file diff --git a/docs/maps/connect-to-ems.asciidoc b/docs/maps/connect-to-ems.asciidoc index e41d544d64e4d..1ccdedb1da2a9 100644 --- a/docs/maps/connect-to-ems.asciidoc +++ b/docs/maps/connect-to-ems.asciidoc @@ -1,6 +1,6 @@ :ems: Elastic Maps Service :ems-docker-repo: docker.elastic.co/elastic-maps-service/elastic-maps-server -:ems-docker-image: {ems-docker-repo}:{version}-amd64 +:ems-docker-image: {ems-docker-repo}:{version} :ems-headers-url: https://deployment-host [[maps-connect-to-ems]] @@ -81,34 +81,53 @@ If you cannot connect to {ems} from the {kib} server or browser clients, and you {hosted-ems} is a self-managed version of {ems} offered as a Docker image that provides both the EMS basemaps and EMS boundaries. The image is bundled with basemaps up to zoom level 8. After connecting it to your {es} cluster for license validation, you have the option to download and configure a more detailed basemaps database. -You can use +docker pull+ to download the {hosted-ems} image from the Elastic Docker registry. - +. Pull the {hosted-ems} Docker image. ++ ifeval::["{release-state}"=="unreleased"] -Version {version} of {hosted-ems} has not yet been released, so no Docker image is currently available for this version. +WARNING: Version {version} of {hosted-ems} has not yet been released. +No Docker image is currently available for this version. endif::[] - -ifeval::["{release-state}"!="unreleased"] - ++ ["source","bash",subs="attributes"] ---------------------------------- docker pull {ems-docker-image} ---------------------------------- -Start {hosted-ems} and expose the default port `8080`: +. Optional: Install +https://docs.sigstore.dev/system_config/installation/[Cosign] for your +environment. Then use Cosign to verify the {es} image's signature. ++ +[source,sh,subs="attributes"] +---- +wget https://artifacts.elastic.co/cosign.pub +cosign verify --key cosign.pub {ems-docker-image} +---- ++ +The `cosign` command prints the check results and the signature payload in JSON format: ++ +[source,sh,subs="attributes"] +-------------------------------------------- +Verification for {ems-docker-image} -- +The following checks were performed on each of these signatures: + - The cosign claims were validated + - Existence of the claims in the transparency log was verified offline + - The signatures were verified against the specified public key +-------------------------------------------- + +. Start {hosted-ems} and expose the default port `8080`: ++ ["source","bash",subs="attributes"] ---------------------------------- docker run --rm --init --publish 8080:8080 \ {ems-docker-image} ---------------------------------- - ++ Once {hosted-ems} is running, follow instructions from the webpage at `localhost:8080` to define a configuration file and optionally download a more detailed basemaps database. - ++ [role="screenshot"] image::images/elastic-maps-server-instructions.png[Set-up instructions] -endif::[] - [float] [[elastic-maps-server-configuration]] ==== Configuration @@ -193,7 +212,6 @@ One way to configure {hosted-ems} is to provide `elastic-maps-server.yml` via bi ["source","yaml",subs="attributes"] -------------------------------------------- -version: '2' services: ems-server: image: {ems-docker-image} @@ -212,7 +230,6 @@ These variables can be set with +docker-compose+ like this: ["source","yaml",subs="attributes"] ---------------------------------------------------------- -version: '2' services: ems-server: image: {ems-docker-image} diff --git a/docs/maps/images/elastic-maps-server-instructions.png b/docs/maps/images/elastic-maps-server-instructions.png index 5c0b47ce8f49f..524ae2192b5e5 100644 Binary files a/docs/maps/images/elastic-maps-server-instructions.png and b/docs/maps/images/elastic-maps-server-instructions.png differ diff --git a/docs/search/index.asciidoc b/docs/search/index.asciidoc index f046330ac13e9..ab4b007800da4 100644 --- a/docs/search/index.asciidoc +++ b/docs/search/index.asciidoc @@ -9,8 +9,8 @@ The *Search* space in {kib} comprises the following features: * <> * https://www.elastic.co/guide/en/elasticsearch/reference/current/search-application-overview.html[Search Applications] * https://www.elastic.co/guide/en/elasticsearch/reference/current/behavioral-analytics-overview.html[Behavioral Analytics] -* Inference Endpoints UI -* AI Assistant for Search +* <> +* <> * Persistent Dev Tools <> [float] @@ -19,53 +19,53 @@ The *Search* space in {kib} comprises the following features: The Search solution and use case is made up of many tools and features across the {stack}. As a result, the release notes for your features of interest might live in different Elastic docs. -// Use the following table to find links to the appropriate documentation, API references (if applicable), and release notes. +Use the following table to find links to the appropriate documentation, API references (if applicable), and release notes. -// [options="header"] -// |=== -// | Name | API reference | Documentation | Release notes +[options="header"] +|=== +| Name | API reference | Documentation | Release notes -// | Connectors -// | link:https://example.com/connectors/api[API reference] -// | link:https://example.com/connectors/docs[Documentation] -// | link:https://example.com/connectors/notes[Release notes] +| Connectors +| {ref}/connector-apis.html[API reference] +| {ref}/es-connectors.html[Elastic Connectors] +| {ref}/es-connectors-release-notes.html[Elasticsearch guide] -// | Web crawler -// | link:https://example.com/web_crawlers/api[API reference] -// | link:https://example.com/web_crawlers/docs[Documentation] -// | link:https://example.com/web_crawlers/notes[Release notes] +| Web crawler +| N/A +| {enterprise-search-ref}/crawler.html[Documentation] +| {enterprise-search-ref}/changelog.html[Enterprise Search Guide] -// | Playground -// | link:https://example.com/playground/api[API reference] -// | link:https://example.com/playground/docs[Documentation] -// | link:https://example.com/playground/notes[Release notes] +| Playground +| N/A +| {kibana-ref}/playground.html[Documentation] +| {kibana-ref}/release-notes.html[Kibana guide] -// | Search Applications -// | link:https://example.com/search_apps/api[API reference] -// | link:https://example.com/search_apps/docs[Documentation] -// | link:https://example.com/search_apps/notes[Release notes] +| Search Applications +| {ref}/search-application-apis.html[API reference] +| {enterprise-search-ref}/app-search-workplace-search.html[Documentation] +| {ref}/es-release-notes.html[Elasticsearch guide] -// | Behavioral Analytics -// | link:https://example.com/behavioral_analytics/api[API reference] -// | link:https://example.com/behavioral_analytics/docs[Documentation] -// | link:https://example.com/behavioral_analytics/notes[Release notes] +| Behavioral Analytics +| {ref}/behavioral-analytics-apis.html[API reference] +| {ref}/behavioral-analytics-start.html[Documentation] +| {ref}/es-release-notes.html[Elasticsearch guide] -// | Inference Endpoints -// | link:https://example.com/inference_endpoints/api[API reference] -// | link:https://example.com/inference_endpoints/docs[Documentation] -// | link:https://example.com/inference_endpoints/notes[Release notes] +| Inference Endpoints +| {ref}/inference-apis.html[API reference] +| {kibana-ref}/inference-endpoints.html[Documentation] +| {ref}/es-release-notes.html[Elasticsearch guide] -// | Console -// | link:https://example.com/console/api[API reference] -// | link:https://example.com/console/docs[Documentation] -// | link:https://example.com/console/notes[Release notes] +| Console +| N/A +| {kibana-ref}/console-kibana.html[Documentation] +| {kibana-ref}/release-notes.html[Kibana guide] -// | Search UI -// | link:https://www.elastic.co/docs/current/search-ui/api/architecture[API reference] -// | link:https://www.elastic.co/docs/current/search-ui/overview[Documentation] -// | link:https://example.com/search_ui/notes[Release notes] +| Search UI +| https://www.elastic.co/docs/current/search-ui/api/architecture[API reference] +| https://www.elastic.co/docs/current/search-ui[Documentation] +| https://www.elastic.co/docs/current/search-ui[Search UI] -// |=== +|=== include::search-connection-details.asciidoc[] include::playground/index.asciidoc[] diff --git a/oas_docs/bundle.json b/oas_docs/bundle.json index 6cc3990de1b51..e52362ff13a6a 100644 --- a/oas_docs/bundle.json +++ b/oas_docs/bundle.json @@ -19328,6 +19328,27 @@ "description": { "type": "string" }, + "discovery": { + "additionalProperties": true, + "properties": { + "fields": { + "items": { + "additionalProperties": true, + "properties": { + "name": { + "type": "string" + } + }, + "required": [ + "name" + ], + "type": "object" + }, + "type": "array" + } + }, + "type": "object" + }, "download": { "type": "string" }, @@ -19716,7 +19737,8 @@ "type": { "enum": [ "integration", - "input" + "input", + "content" ], "type": "string" }, @@ -19793,6 +19815,27 @@ "description": { "type": "string" }, + "discovery": { + "additionalProperties": true, + "properties": { + "fields": { + "items": { + "additionalProperties": true, + "properties": { + "name": { + "type": "string" + } + }, + "required": [ + "name" + ], + "type": "object" + }, + "type": "array" + } + }, + "type": "object" + }, "download": { "type": "string" }, @@ -20181,7 +20224,8 @@ "type": { "enum": [ "integration", - "input" + "input", + "content" ], "type": "string" }, @@ -21769,6 +21813,27 @@ "description": { "type": "string" }, + "discovery": { + "additionalProperties": true, + "properties": { + "fields": { + "items": { + "additionalProperties": true, + "properties": { + "name": { + "type": "string" + } + }, + "required": [ + "name" + ], + "type": "object" + }, + "type": "array" + } + }, + "type": "object" + }, "download": { "type": "string" }, @@ -22197,7 +22262,8 @@ "type": { "enum": [ "integration", - "input" + "input", + "content" ], "type": "string" }, @@ -22329,6 +22395,27 @@ "description": { "type": "string" }, + "discovery": { + "additionalProperties": true, + "properties": { + "fields": { + "items": { + "additionalProperties": true, + "properties": { + "name": { + "type": "string" + } + }, + "required": [ + "name" + ], + "type": "object" + }, + "type": "array" + } + }, + "type": "object" + }, "download": { "type": "string" }, @@ -22757,7 +22844,8 @@ "type": { "enum": [ "integration", - "input" + "input", + "content" ], "type": "string" }, @@ -23279,6 +23367,27 @@ "description": { "type": "string" }, + "discovery": { + "additionalProperties": true, + "properties": { + "fields": { + "items": { + "additionalProperties": true, + "properties": { + "name": { + "type": "string" + } + }, + "required": [ + "name" + ], + "type": "object" + }, + "type": "array" + } + }, + "type": "object" + }, "download": { "type": "string" }, @@ -23707,7 +23816,8 @@ "type": { "enum": [ "integration", - "input" + "input", + "content" ], "type": "string" }, @@ -23827,6 +23937,27 @@ "description": { "type": "string" }, + "discovery": { + "additionalProperties": true, + "properties": { + "fields": { + "items": { + "additionalProperties": true, + "properties": { + "name": { + "type": "string" + } + }, + "required": [ + "name" + ], + "type": "object" + }, + "type": "array" + } + }, + "type": "object" + }, "download": { "type": "string" }, @@ -24255,7 +24386,8 @@ "type": { "enum": [ "integration", - "input" + "input", + "content" ], "type": "string" }, diff --git a/oas_docs/bundle.serverless.json b/oas_docs/bundle.serverless.json index 6fcc247e1fb22..531ab412ce1bf 100644 --- a/oas_docs/bundle.serverless.json +++ b/oas_docs/bundle.serverless.json @@ -19328,6 +19328,27 @@ "description": { "type": "string" }, + "discovery": { + "additionalProperties": true, + "properties": { + "fields": { + "items": { + "additionalProperties": true, + "properties": { + "name": { + "type": "string" + } + }, + "required": [ + "name" + ], + "type": "object" + }, + "type": "array" + } + }, + "type": "object" + }, "download": { "type": "string" }, @@ -19716,7 +19737,8 @@ "type": { "enum": [ "integration", - "input" + "input", + "content" ], "type": "string" }, @@ -19793,6 +19815,27 @@ "description": { "type": "string" }, + "discovery": { + "additionalProperties": true, + "properties": { + "fields": { + "items": { + "additionalProperties": true, + "properties": { + "name": { + "type": "string" + } + }, + "required": [ + "name" + ], + "type": "object" + }, + "type": "array" + } + }, + "type": "object" + }, "download": { "type": "string" }, @@ -20181,7 +20224,8 @@ "type": { "enum": [ "integration", - "input" + "input", + "content" ], "type": "string" }, @@ -21769,6 +21813,27 @@ "description": { "type": "string" }, + "discovery": { + "additionalProperties": true, + "properties": { + "fields": { + "items": { + "additionalProperties": true, + "properties": { + "name": { + "type": "string" + } + }, + "required": [ + "name" + ], + "type": "object" + }, + "type": "array" + } + }, + "type": "object" + }, "download": { "type": "string" }, @@ -22197,7 +22262,8 @@ "type": { "enum": [ "integration", - "input" + "input", + "content" ], "type": "string" }, @@ -22329,6 +22395,27 @@ "description": { "type": "string" }, + "discovery": { + "additionalProperties": true, + "properties": { + "fields": { + "items": { + "additionalProperties": true, + "properties": { + "name": { + "type": "string" + } + }, + "required": [ + "name" + ], + "type": "object" + }, + "type": "array" + } + }, + "type": "object" + }, "download": { "type": "string" }, @@ -22757,7 +22844,8 @@ "type": { "enum": [ "integration", - "input" + "input", + "content" ], "type": "string" }, @@ -23279,6 +23367,27 @@ "description": { "type": "string" }, + "discovery": { + "additionalProperties": true, + "properties": { + "fields": { + "items": { + "additionalProperties": true, + "properties": { + "name": { + "type": "string" + } + }, + "required": [ + "name" + ], + "type": "object" + }, + "type": "array" + } + }, + "type": "object" + }, "download": { "type": "string" }, @@ -23707,7 +23816,8 @@ "type": { "enum": [ "integration", - "input" + "input", + "content" ], "type": "string" }, @@ -23827,6 +23937,27 @@ "description": { "type": "string" }, + "discovery": { + "additionalProperties": true, + "properties": { + "fields": { + "items": { + "additionalProperties": true, + "properties": { + "name": { + "type": "string" + } + }, + "required": [ + "name" + ], + "type": "object" + }, + "type": "array" + } + }, + "type": "object" + }, "download": { "type": "string" }, @@ -24255,7 +24386,8 @@ "type": { "enum": [ "integration", - "input" + "input", + "content" ], "type": "string" }, diff --git a/oas_docs/output/kibana.serverless.staging.yaml b/oas_docs/output/kibana.serverless.staging.yaml index 67e2f6844a6df..20ab121c161bd 100644 --- a/oas_docs/output/kibana.serverless.staging.yaml +++ b/oas_docs/output/kibana.serverless.staging.yaml @@ -8080,6 +8080,7 @@ paths: - Security Endpoint Management API /api/endpoint/metadata/transforms: get: + deprecated: true operationId: GetEndpointMetadataTransform responses: '200': @@ -18989,6 +18990,20 @@ paths: type: array description: type: string + discovery: + additionalProperties: true + type: object + properties: + fields: + items: + additionalProperties: true + type: object + properties: + name: + type: string + required: + - name + type: array download: type: string format_version: @@ -19269,6 +19284,7 @@ paths: enum: - integration - input + - content type: string vars: items: @@ -19321,6 +19337,20 @@ paths: type: array description: type: string + discovery: + additionalProperties: true + type: object + properties: + fields: + items: + additionalProperties: true + type: object + properties: + name: + type: string + required: + - name + type: array download: type: string format_version: @@ -19601,6 +19631,7 @@ paths: enum: - integration - input + - content type: string vars: items: @@ -20573,6 +20604,20 @@ paths: type: array description: type: string + discovery: + additionalProperties: true + type: object + properties: + fields: + items: + additionalProperties: true + type: object + properties: + name: + type: string + required: + - name + type: array download: type: string elasticsearch: @@ -20880,6 +20925,7 @@ paths: enum: - integration - input + - content type: string vars: items: @@ -20969,6 +21015,20 @@ paths: type: array description: type: string + discovery: + additionalProperties: true + type: object + properties: + fields: + items: + additionalProperties: true + type: object + properties: + name: + type: string + required: + - name + type: array download: type: string elasticsearch: @@ -21276,6 +21336,7 @@ paths: enum: - integration - input + - content type: string vars: items: @@ -21628,6 +21689,20 @@ paths: type: array description: type: string + discovery: + additionalProperties: true + type: object + properties: + fields: + items: + additionalProperties: true + type: object + properties: + name: + type: string + required: + - name + type: array download: type: string elasticsearch: @@ -21935,6 +22010,7 @@ paths: enum: - integration - input + - content type: string vars: items: @@ -22016,6 +22092,20 @@ paths: type: array description: type: string + discovery: + additionalProperties: true + type: object + properties: + fields: + items: + additionalProperties: true + type: object + properties: + name: + type: string + required: + - name + type: array download: type: string elasticsearch: @@ -22323,6 +22413,7 @@ paths: enum: - integration - input + - content type: string vars: items: @@ -40800,6 +40891,7 @@ components: enum: - OpenAI - Azure OpenAI + - Other type: string Security_AI_Assistant_API_Reader: additionalProperties: true @@ -41386,10 +41478,6 @@ components: $ref: '#/components/schemas/Security_Detections_API_RuleFilterArray' index: $ref: '#/components/schemas/Security_Detections_API_IndexPatternArray' - response_actions: - items: - $ref: '#/components/schemas/Security_Detections_API_ResponseAction' - type: array tiebreaker_field: $ref: '#/components/schemas/Security_Detections_API_TiebreakerField' timestamp_field: @@ -41479,6 +41567,10 @@ components: $ref: >- #/components/schemas/Security_Detections_API_RequiredFieldInput type: array + response_actions: + items: + $ref: '#/components/schemas/Security_Detections_API_ResponseAction' + type: array risk_score: $ref: '#/components/schemas/Security_Detections_API_RiskScore' risk_score_mapping: @@ -41603,6 +41695,10 @@ components: $ref: >- #/components/schemas/Security_Detections_API_RequiredFieldInput type: array + response_actions: + items: + $ref: '#/components/schemas/Security_Detections_API_ResponseAction' + type: array risk_score: $ref: '#/components/schemas/Security_Detections_API_RiskScore' risk_score_mapping: @@ -41724,6 +41820,10 @@ components: $ref: >- #/components/schemas/Security_Detections_API_RequiredFieldInput type: array + response_actions: + items: + $ref: '#/components/schemas/Security_Detections_API_ResponseAction' + type: array risk_score: $ref: '#/components/schemas/Security_Detections_API_RiskScore' risk_score_mapping: @@ -41828,6 +41928,10 @@ components: $ref: >- #/components/schemas/Security_Detections_API_RequiredFieldInput type: array + response_actions: + items: + $ref: '#/components/schemas/Security_Detections_API_ResponseAction' + type: array risk_score: $ref: '#/components/schemas/Security_Detections_API_RiskScore' risk_score_mapping: @@ -41962,6 +42066,10 @@ components: $ref: >- #/components/schemas/Security_Detections_API_RequiredFieldInput type: array + response_actions: + items: + $ref: '#/components/schemas/Security_Detections_API_ResponseAction' + type: array risk_score: $ref: '#/components/schemas/Security_Detections_API_RiskScore' risk_score_mapping: @@ -42086,6 +42194,10 @@ components: $ref: >- #/components/schemas/Security_Detections_API_RequiredFieldInput type: array + response_actions: + items: + $ref: '#/components/schemas/Security_Detections_API_ResponseAction' + type: array risk_score: $ref: '#/components/schemas/Security_Detections_API_RiskScore' risk_score_mapping: @@ -42131,10 +42243,6 @@ components: properties: alert_suppression: $ref: '#/components/schemas/Security_Detections_API_AlertSuppression' - response_actions: - items: - $ref: '#/components/schemas/Security_Detections_API_ResponseAction' - type: array Security_Detections_API_EsqlRulePatchProps: allOf: - type: object @@ -42205,6 +42313,10 @@ components: $ref: >- #/components/schemas/Security_Detections_API_RequiredFieldInput type: array + response_actions: + items: + $ref: '#/components/schemas/Security_Detections_API_ResponseAction' + type: array risk_score: $ref: '#/components/schemas/Security_Detections_API_RiskScore' risk_score_mapping: @@ -42331,6 +42443,10 @@ components: $ref: >- #/components/schemas/Security_Detections_API_RequiredFieldInput type: array + response_actions: + items: + $ref: '#/components/schemas/Security_Detections_API_ResponseAction' + type: array risk_score: $ref: '#/components/schemas/Security_Detections_API_RiskScore' risk_score_mapping: @@ -42570,6 +42686,10 @@ components: $ref: >- #/components/schemas/Security_Detections_API_RequiredFieldInput type: array + response_actions: + items: + $ref: '#/components/schemas/Security_Detections_API_ResponseAction' + type: array risk_score: $ref: '#/components/schemas/Security_Detections_API_RiskScore' risk_score_mapping: @@ -42697,6 +42817,10 @@ components: $ref: >- #/components/schemas/Security_Detections_API_RequiredFieldInput type: array + response_actions: + items: + $ref: '#/components/schemas/Security_Detections_API_ResponseAction' + type: array risk_score: $ref: '#/components/schemas/Security_Detections_API_RiskScore' risk_score_mapping: @@ -42824,6 +42948,10 @@ components: $ref: >- #/components/schemas/Security_Detections_API_RequiredFieldInput type: array + response_actions: + items: + $ref: '#/components/schemas/Security_Detections_API_ResponseAction' + type: array risk_score: $ref: '#/components/schemas/Security_Detections_API_RiskScore' risk_score_mapping: @@ -42947,6 +43075,10 @@ components: $ref: >- #/components/schemas/Security_Detections_API_RequiredFieldInput type: array + response_actions: + items: + $ref: '#/components/schemas/Security_Detections_API_ResponseAction' + type: array risk_score: $ref: '#/components/schemas/Security_Detections_API_RiskScore' risk_score_mapping: @@ -43060,6 +43192,10 @@ components: $ref: >- #/components/schemas/Security_Detections_API_RequiredFieldInput type: array + response_actions: + items: + $ref: '#/components/schemas/Security_Detections_API_ResponseAction' + type: array risk_score: $ref: '#/components/schemas/Security_Detections_API_RiskScore' risk_score_mapping: @@ -43189,6 +43325,10 @@ components: $ref: >- #/components/schemas/Security_Detections_API_RequiredFieldInput type: array + response_actions: + items: + $ref: '#/components/schemas/Security_Detections_API_ResponseAction' + type: array risk_score: $ref: '#/components/schemas/Security_Detections_API_RiskScore' risk_score_mapping: @@ -43246,10 +43386,6 @@ components: $ref: '#/components/schemas/Security_Detections_API_RuleFilterArray' index: $ref: '#/components/schemas/Security_Detections_API_IndexPatternArray' - response_actions: - items: - $ref: '#/components/schemas/Security_Detections_API_ResponseAction' - type: array Security_Detections_API_NewTermsRulePatchFields: allOf: - type: object @@ -43334,6 +43470,10 @@ components: $ref: >- #/components/schemas/Security_Detections_API_RequiredFieldInput type: array + response_actions: + items: + $ref: '#/components/schemas/Security_Detections_API_ResponseAction' + type: array risk_score: $ref: '#/components/schemas/Security_Detections_API_RiskScore' risk_score_mapping: @@ -43465,6 +43605,10 @@ components: $ref: >- #/components/schemas/Security_Detections_API_RequiredFieldInput type: array + response_actions: + items: + $ref: '#/components/schemas/Security_Detections_API_ResponseAction' + type: array risk_score: $ref: '#/components/schemas/Security_Detections_API_RiskScore' risk_score_mapping: @@ -43699,6 +43843,10 @@ components: $ref: >- #/components/schemas/Security_Detections_API_RequiredFieldInput type: array + response_actions: + items: + $ref: '#/components/schemas/Security_Detections_API_ResponseAction' + type: array risk_score: $ref: '#/components/schemas/Security_Detections_API_RiskScore' risk_score_mapping: @@ -43825,6 +43973,10 @@ components: $ref: >- #/components/schemas/Security_Detections_API_RequiredFieldInput type: array + response_actions: + items: + $ref: '#/components/schemas/Security_Detections_API_ResponseAction' + type: array risk_score: $ref: '#/components/schemas/Security_Detections_API_RiskScore' risk_score_mapping: @@ -43883,10 +44035,6 @@ components: $ref: '#/components/schemas/Security_Detections_API_RuleFilterArray' index: $ref: '#/components/schemas/Security_Detections_API_IndexPatternArray' - response_actions: - items: - $ref: '#/components/schemas/Security_Detections_API_ResponseAction' - type: array saved_id: $ref: '#/components/schemas/Security_Detections_API_SavedQueryId' Security_Detections_API_QueryRulePatchFields: @@ -43966,6 +44114,10 @@ components: $ref: >- #/components/schemas/Security_Detections_API_RequiredFieldInput type: array + response_actions: + items: + $ref: '#/components/schemas/Security_Detections_API_ResponseAction' + type: array risk_score: $ref: '#/components/schemas/Security_Detections_API_RiskScore' risk_score_mapping: @@ -44089,6 +44241,10 @@ components: $ref: >- #/components/schemas/Security_Detections_API_RequiredFieldInput type: array + response_actions: + items: + $ref: '#/components/schemas/Security_Detections_API_ResponseAction' + type: array risk_score: $ref: '#/components/schemas/Security_Detections_API_RiskScore' risk_score_mapping: @@ -44798,6 +44954,10 @@ components: $ref: >- #/components/schemas/Security_Detections_API_RequiredFieldInput type: array + response_actions: + items: + $ref: '#/components/schemas/Security_Detections_API_ResponseAction' + type: array risk_score: $ref: '#/components/schemas/Security_Detections_API_RiskScore' risk_score_mapping: @@ -44927,6 +45087,10 @@ components: $ref: >- #/components/schemas/Security_Detections_API_RequiredFieldInput type: array + response_actions: + items: + $ref: '#/components/schemas/Security_Detections_API_ResponseAction' + type: array risk_score: $ref: '#/components/schemas/Security_Detections_API_RiskScore' risk_score_mapping: @@ -44986,10 +45150,6 @@ components: $ref: '#/components/schemas/Security_Detections_API_IndexPatternArray' query: $ref: '#/components/schemas/Security_Detections_API_RuleQuery' - response_actions: - items: - $ref: '#/components/schemas/Security_Detections_API_ResponseAction' - type: array Security_Detections_API_SavedQueryRulePatchFields: allOf: - type: object @@ -45070,6 +45230,10 @@ components: $ref: >- #/components/schemas/Security_Detections_API_RequiredFieldInput type: array + response_actions: + items: + $ref: '#/components/schemas/Security_Detections_API_ResponseAction' + type: array risk_score: $ref: '#/components/schemas/Security_Detections_API_RiskScore' risk_score_mapping: @@ -45196,6 +45360,10 @@ components: $ref: >- #/components/schemas/Security_Detections_API_RequiredFieldInput type: array + response_actions: + items: + $ref: '#/components/schemas/Security_Detections_API_ResponseAction' + type: array risk_score: $ref: '#/components/schemas/Security_Detections_API_RiskScore' risk_score_mapping: @@ -45446,6 +45614,10 @@ components: $ref: >- #/components/schemas/Security_Detections_API_RequiredFieldInput type: array + response_actions: + items: + $ref: '#/components/schemas/Security_Detections_API_ResponseAction' + type: array risk_score: $ref: '#/components/schemas/Security_Detections_API_RiskScore' risk_score_mapping: @@ -45575,6 +45747,10 @@ components: $ref: >- #/components/schemas/Security_Detections_API_RequiredFieldInput type: array + response_actions: + items: + $ref: '#/components/schemas/Security_Detections_API_ResponseAction' + type: array risk_score: $ref: '#/components/schemas/Security_Detections_API_RiskScore' risk_score_mapping: @@ -45730,6 +45906,10 @@ components: $ref: >- #/components/schemas/Security_Detections_API_RequiredFieldInput type: array + response_actions: + items: + $ref: '#/components/schemas/Security_Detections_API_ResponseAction' + type: array risk_score: $ref: '#/components/schemas/Security_Detections_API_RiskScore' risk_score_mapping: @@ -45865,6 +46045,10 @@ components: $ref: >- #/components/schemas/Security_Detections_API_RequiredFieldInput type: array + response_actions: + items: + $ref: '#/components/schemas/Security_Detections_API_ResponseAction' + type: array risk_score: $ref: '#/components/schemas/Security_Detections_API_RiskScore' risk_score_mapping: @@ -46065,6 +46249,10 @@ components: $ref: >- #/components/schemas/Security_Detections_API_RequiredFieldInput type: array + response_actions: + items: + $ref: '#/components/schemas/Security_Detections_API_ResponseAction' + type: array risk_score: $ref: '#/components/schemas/Security_Detections_API_RiskScore' risk_score_mapping: @@ -46194,6 +46382,10 @@ components: $ref: >- #/components/schemas/Security_Detections_API_RequiredFieldInput type: array + response_actions: + items: + $ref: '#/components/schemas/Security_Detections_API_ResponseAction' + type: array risk_score: $ref: '#/components/schemas/Security_Detections_API_RiskScore' risk_score_mapping: @@ -46336,6 +46528,10 @@ components: $ref: >- #/components/schemas/Security_Detections_API_RequiredFieldInput type: array + response_actions: + items: + $ref: '#/components/schemas/Security_Detections_API_ResponseAction' + type: array risk_score: $ref: '#/components/schemas/Security_Detections_API_RiskScore' risk_score_mapping: @@ -46465,6 +46661,10 @@ components: $ref: >- #/components/schemas/Security_Detections_API_RequiredFieldInput type: array + response_actions: + items: + $ref: '#/components/schemas/Security_Detections_API_ResponseAction' + type: array risk_score: $ref: '#/components/schemas/Security_Detections_API_RiskScore' risk_score_mapping: diff --git a/oas_docs/output/kibana.serverless.yaml b/oas_docs/output/kibana.serverless.yaml index 67e2f6844a6df..20ab121c161bd 100644 --- a/oas_docs/output/kibana.serverless.yaml +++ b/oas_docs/output/kibana.serverless.yaml @@ -8080,6 +8080,7 @@ paths: - Security Endpoint Management API /api/endpoint/metadata/transforms: get: + deprecated: true operationId: GetEndpointMetadataTransform responses: '200': @@ -18989,6 +18990,20 @@ paths: type: array description: type: string + discovery: + additionalProperties: true + type: object + properties: + fields: + items: + additionalProperties: true + type: object + properties: + name: + type: string + required: + - name + type: array download: type: string format_version: @@ -19269,6 +19284,7 @@ paths: enum: - integration - input + - content type: string vars: items: @@ -19321,6 +19337,20 @@ paths: type: array description: type: string + discovery: + additionalProperties: true + type: object + properties: + fields: + items: + additionalProperties: true + type: object + properties: + name: + type: string + required: + - name + type: array download: type: string format_version: @@ -19601,6 +19631,7 @@ paths: enum: - integration - input + - content type: string vars: items: @@ -20573,6 +20604,20 @@ paths: type: array description: type: string + discovery: + additionalProperties: true + type: object + properties: + fields: + items: + additionalProperties: true + type: object + properties: + name: + type: string + required: + - name + type: array download: type: string elasticsearch: @@ -20880,6 +20925,7 @@ paths: enum: - integration - input + - content type: string vars: items: @@ -20969,6 +21015,20 @@ paths: type: array description: type: string + discovery: + additionalProperties: true + type: object + properties: + fields: + items: + additionalProperties: true + type: object + properties: + name: + type: string + required: + - name + type: array download: type: string elasticsearch: @@ -21276,6 +21336,7 @@ paths: enum: - integration - input + - content type: string vars: items: @@ -21628,6 +21689,20 @@ paths: type: array description: type: string + discovery: + additionalProperties: true + type: object + properties: + fields: + items: + additionalProperties: true + type: object + properties: + name: + type: string + required: + - name + type: array download: type: string elasticsearch: @@ -21935,6 +22010,7 @@ paths: enum: - integration - input + - content type: string vars: items: @@ -22016,6 +22092,20 @@ paths: type: array description: type: string + discovery: + additionalProperties: true + type: object + properties: + fields: + items: + additionalProperties: true + type: object + properties: + name: + type: string + required: + - name + type: array download: type: string elasticsearch: @@ -22323,6 +22413,7 @@ paths: enum: - integration - input + - content type: string vars: items: @@ -40800,6 +40891,7 @@ components: enum: - OpenAI - Azure OpenAI + - Other type: string Security_AI_Assistant_API_Reader: additionalProperties: true @@ -41386,10 +41478,6 @@ components: $ref: '#/components/schemas/Security_Detections_API_RuleFilterArray' index: $ref: '#/components/schemas/Security_Detections_API_IndexPatternArray' - response_actions: - items: - $ref: '#/components/schemas/Security_Detections_API_ResponseAction' - type: array tiebreaker_field: $ref: '#/components/schemas/Security_Detections_API_TiebreakerField' timestamp_field: @@ -41479,6 +41567,10 @@ components: $ref: >- #/components/schemas/Security_Detections_API_RequiredFieldInput type: array + response_actions: + items: + $ref: '#/components/schemas/Security_Detections_API_ResponseAction' + type: array risk_score: $ref: '#/components/schemas/Security_Detections_API_RiskScore' risk_score_mapping: @@ -41603,6 +41695,10 @@ components: $ref: >- #/components/schemas/Security_Detections_API_RequiredFieldInput type: array + response_actions: + items: + $ref: '#/components/schemas/Security_Detections_API_ResponseAction' + type: array risk_score: $ref: '#/components/schemas/Security_Detections_API_RiskScore' risk_score_mapping: @@ -41724,6 +41820,10 @@ components: $ref: >- #/components/schemas/Security_Detections_API_RequiredFieldInput type: array + response_actions: + items: + $ref: '#/components/schemas/Security_Detections_API_ResponseAction' + type: array risk_score: $ref: '#/components/schemas/Security_Detections_API_RiskScore' risk_score_mapping: @@ -41828,6 +41928,10 @@ components: $ref: >- #/components/schemas/Security_Detections_API_RequiredFieldInput type: array + response_actions: + items: + $ref: '#/components/schemas/Security_Detections_API_ResponseAction' + type: array risk_score: $ref: '#/components/schemas/Security_Detections_API_RiskScore' risk_score_mapping: @@ -41962,6 +42066,10 @@ components: $ref: >- #/components/schemas/Security_Detections_API_RequiredFieldInput type: array + response_actions: + items: + $ref: '#/components/schemas/Security_Detections_API_ResponseAction' + type: array risk_score: $ref: '#/components/schemas/Security_Detections_API_RiskScore' risk_score_mapping: @@ -42086,6 +42194,10 @@ components: $ref: >- #/components/schemas/Security_Detections_API_RequiredFieldInput type: array + response_actions: + items: + $ref: '#/components/schemas/Security_Detections_API_ResponseAction' + type: array risk_score: $ref: '#/components/schemas/Security_Detections_API_RiskScore' risk_score_mapping: @@ -42131,10 +42243,6 @@ components: properties: alert_suppression: $ref: '#/components/schemas/Security_Detections_API_AlertSuppression' - response_actions: - items: - $ref: '#/components/schemas/Security_Detections_API_ResponseAction' - type: array Security_Detections_API_EsqlRulePatchProps: allOf: - type: object @@ -42205,6 +42313,10 @@ components: $ref: >- #/components/schemas/Security_Detections_API_RequiredFieldInput type: array + response_actions: + items: + $ref: '#/components/schemas/Security_Detections_API_ResponseAction' + type: array risk_score: $ref: '#/components/schemas/Security_Detections_API_RiskScore' risk_score_mapping: @@ -42331,6 +42443,10 @@ components: $ref: >- #/components/schemas/Security_Detections_API_RequiredFieldInput type: array + response_actions: + items: + $ref: '#/components/schemas/Security_Detections_API_ResponseAction' + type: array risk_score: $ref: '#/components/schemas/Security_Detections_API_RiskScore' risk_score_mapping: @@ -42570,6 +42686,10 @@ components: $ref: >- #/components/schemas/Security_Detections_API_RequiredFieldInput type: array + response_actions: + items: + $ref: '#/components/schemas/Security_Detections_API_ResponseAction' + type: array risk_score: $ref: '#/components/schemas/Security_Detections_API_RiskScore' risk_score_mapping: @@ -42697,6 +42817,10 @@ components: $ref: >- #/components/schemas/Security_Detections_API_RequiredFieldInput type: array + response_actions: + items: + $ref: '#/components/schemas/Security_Detections_API_ResponseAction' + type: array risk_score: $ref: '#/components/schemas/Security_Detections_API_RiskScore' risk_score_mapping: @@ -42824,6 +42948,10 @@ components: $ref: >- #/components/schemas/Security_Detections_API_RequiredFieldInput type: array + response_actions: + items: + $ref: '#/components/schemas/Security_Detections_API_ResponseAction' + type: array risk_score: $ref: '#/components/schemas/Security_Detections_API_RiskScore' risk_score_mapping: @@ -42947,6 +43075,10 @@ components: $ref: >- #/components/schemas/Security_Detections_API_RequiredFieldInput type: array + response_actions: + items: + $ref: '#/components/schemas/Security_Detections_API_ResponseAction' + type: array risk_score: $ref: '#/components/schemas/Security_Detections_API_RiskScore' risk_score_mapping: @@ -43060,6 +43192,10 @@ components: $ref: >- #/components/schemas/Security_Detections_API_RequiredFieldInput type: array + response_actions: + items: + $ref: '#/components/schemas/Security_Detections_API_ResponseAction' + type: array risk_score: $ref: '#/components/schemas/Security_Detections_API_RiskScore' risk_score_mapping: @@ -43189,6 +43325,10 @@ components: $ref: >- #/components/schemas/Security_Detections_API_RequiredFieldInput type: array + response_actions: + items: + $ref: '#/components/schemas/Security_Detections_API_ResponseAction' + type: array risk_score: $ref: '#/components/schemas/Security_Detections_API_RiskScore' risk_score_mapping: @@ -43246,10 +43386,6 @@ components: $ref: '#/components/schemas/Security_Detections_API_RuleFilterArray' index: $ref: '#/components/schemas/Security_Detections_API_IndexPatternArray' - response_actions: - items: - $ref: '#/components/schemas/Security_Detections_API_ResponseAction' - type: array Security_Detections_API_NewTermsRulePatchFields: allOf: - type: object @@ -43334,6 +43470,10 @@ components: $ref: >- #/components/schemas/Security_Detections_API_RequiredFieldInput type: array + response_actions: + items: + $ref: '#/components/schemas/Security_Detections_API_ResponseAction' + type: array risk_score: $ref: '#/components/schemas/Security_Detections_API_RiskScore' risk_score_mapping: @@ -43465,6 +43605,10 @@ components: $ref: >- #/components/schemas/Security_Detections_API_RequiredFieldInput type: array + response_actions: + items: + $ref: '#/components/schemas/Security_Detections_API_ResponseAction' + type: array risk_score: $ref: '#/components/schemas/Security_Detections_API_RiskScore' risk_score_mapping: @@ -43699,6 +43843,10 @@ components: $ref: >- #/components/schemas/Security_Detections_API_RequiredFieldInput type: array + response_actions: + items: + $ref: '#/components/schemas/Security_Detections_API_ResponseAction' + type: array risk_score: $ref: '#/components/schemas/Security_Detections_API_RiskScore' risk_score_mapping: @@ -43825,6 +43973,10 @@ components: $ref: >- #/components/schemas/Security_Detections_API_RequiredFieldInput type: array + response_actions: + items: + $ref: '#/components/schemas/Security_Detections_API_ResponseAction' + type: array risk_score: $ref: '#/components/schemas/Security_Detections_API_RiskScore' risk_score_mapping: @@ -43883,10 +44035,6 @@ components: $ref: '#/components/schemas/Security_Detections_API_RuleFilterArray' index: $ref: '#/components/schemas/Security_Detections_API_IndexPatternArray' - response_actions: - items: - $ref: '#/components/schemas/Security_Detections_API_ResponseAction' - type: array saved_id: $ref: '#/components/schemas/Security_Detections_API_SavedQueryId' Security_Detections_API_QueryRulePatchFields: @@ -43966,6 +44114,10 @@ components: $ref: >- #/components/schemas/Security_Detections_API_RequiredFieldInput type: array + response_actions: + items: + $ref: '#/components/schemas/Security_Detections_API_ResponseAction' + type: array risk_score: $ref: '#/components/schemas/Security_Detections_API_RiskScore' risk_score_mapping: @@ -44089,6 +44241,10 @@ components: $ref: >- #/components/schemas/Security_Detections_API_RequiredFieldInput type: array + response_actions: + items: + $ref: '#/components/schemas/Security_Detections_API_ResponseAction' + type: array risk_score: $ref: '#/components/schemas/Security_Detections_API_RiskScore' risk_score_mapping: @@ -44798,6 +44954,10 @@ components: $ref: >- #/components/schemas/Security_Detections_API_RequiredFieldInput type: array + response_actions: + items: + $ref: '#/components/schemas/Security_Detections_API_ResponseAction' + type: array risk_score: $ref: '#/components/schemas/Security_Detections_API_RiskScore' risk_score_mapping: @@ -44927,6 +45087,10 @@ components: $ref: >- #/components/schemas/Security_Detections_API_RequiredFieldInput type: array + response_actions: + items: + $ref: '#/components/schemas/Security_Detections_API_ResponseAction' + type: array risk_score: $ref: '#/components/schemas/Security_Detections_API_RiskScore' risk_score_mapping: @@ -44986,10 +45150,6 @@ components: $ref: '#/components/schemas/Security_Detections_API_IndexPatternArray' query: $ref: '#/components/schemas/Security_Detections_API_RuleQuery' - response_actions: - items: - $ref: '#/components/schemas/Security_Detections_API_ResponseAction' - type: array Security_Detections_API_SavedQueryRulePatchFields: allOf: - type: object @@ -45070,6 +45230,10 @@ components: $ref: >- #/components/schemas/Security_Detections_API_RequiredFieldInput type: array + response_actions: + items: + $ref: '#/components/schemas/Security_Detections_API_ResponseAction' + type: array risk_score: $ref: '#/components/schemas/Security_Detections_API_RiskScore' risk_score_mapping: @@ -45196,6 +45360,10 @@ components: $ref: >- #/components/schemas/Security_Detections_API_RequiredFieldInput type: array + response_actions: + items: + $ref: '#/components/schemas/Security_Detections_API_ResponseAction' + type: array risk_score: $ref: '#/components/schemas/Security_Detections_API_RiskScore' risk_score_mapping: @@ -45446,6 +45614,10 @@ components: $ref: >- #/components/schemas/Security_Detections_API_RequiredFieldInput type: array + response_actions: + items: + $ref: '#/components/schemas/Security_Detections_API_ResponseAction' + type: array risk_score: $ref: '#/components/schemas/Security_Detections_API_RiskScore' risk_score_mapping: @@ -45575,6 +45747,10 @@ components: $ref: >- #/components/schemas/Security_Detections_API_RequiredFieldInput type: array + response_actions: + items: + $ref: '#/components/schemas/Security_Detections_API_ResponseAction' + type: array risk_score: $ref: '#/components/schemas/Security_Detections_API_RiskScore' risk_score_mapping: @@ -45730,6 +45906,10 @@ components: $ref: >- #/components/schemas/Security_Detections_API_RequiredFieldInput type: array + response_actions: + items: + $ref: '#/components/schemas/Security_Detections_API_ResponseAction' + type: array risk_score: $ref: '#/components/schemas/Security_Detections_API_RiskScore' risk_score_mapping: @@ -45865,6 +46045,10 @@ components: $ref: >- #/components/schemas/Security_Detections_API_RequiredFieldInput type: array + response_actions: + items: + $ref: '#/components/schemas/Security_Detections_API_ResponseAction' + type: array risk_score: $ref: '#/components/schemas/Security_Detections_API_RiskScore' risk_score_mapping: @@ -46065,6 +46249,10 @@ components: $ref: >- #/components/schemas/Security_Detections_API_RequiredFieldInput type: array + response_actions: + items: + $ref: '#/components/schemas/Security_Detections_API_ResponseAction' + type: array risk_score: $ref: '#/components/schemas/Security_Detections_API_RiskScore' risk_score_mapping: @@ -46194,6 +46382,10 @@ components: $ref: >- #/components/schemas/Security_Detections_API_RequiredFieldInput type: array + response_actions: + items: + $ref: '#/components/schemas/Security_Detections_API_ResponseAction' + type: array risk_score: $ref: '#/components/schemas/Security_Detections_API_RiskScore' risk_score_mapping: @@ -46336,6 +46528,10 @@ components: $ref: >- #/components/schemas/Security_Detections_API_RequiredFieldInput type: array + response_actions: + items: + $ref: '#/components/schemas/Security_Detections_API_ResponseAction' + type: array risk_score: $ref: '#/components/schemas/Security_Detections_API_RiskScore' risk_score_mapping: @@ -46465,6 +46661,10 @@ components: $ref: >- #/components/schemas/Security_Detections_API_RequiredFieldInput type: array + response_actions: + items: + $ref: '#/components/schemas/Security_Detections_API_ResponseAction' + type: array risk_score: $ref: '#/components/schemas/Security_Detections_API_RiskScore' risk_score_mapping: diff --git a/oas_docs/output/kibana.staging.yaml b/oas_docs/output/kibana.staging.yaml index 372569024e114..6aa75efa5bd70 100644 --- a/oas_docs/output/kibana.staging.yaml +++ b/oas_docs/output/kibana.staging.yaml @@ -11452,6 +11452,7 @@ paths: - Security Endpoint Management API /api/endpoint/metadata/transforms: get: + deprecated: true operationId: GetEndpointMetadataTransform responses: '200': @@ -22418,6 +22419,20 @@ paths: type: array description: type: string + discovery: + additionalProperties: true + type: object + properties: + fields: + items: + additionalProperties: true + type: object + properties: + name: + type: string + required: + - name + type: array download: type: string format_version: @@ -22698,6 +22713,7 @@ paths: enum: - integration - input + - content type: string vars: items: @@ -22750,6 +22766,20 @@ paths: type: array description: type: string + discovery: + additionalProperties: true + type: object + properties: + fields: + items: + additionalProperties: true + type: object + properties: + name: + type: string + required: + - name + type: array download: type: string format_version: @@ -23030,6 +23060,7 @@ paths: enum: - integration - input + - content type: string vars: items: @@ -24002,6 +24033,20 @@ paths: type: array description: type: string + discovery: + additionalProperties: true + type: object + properties: + fields: + items: + additionalProperties: true + type: object + properties: + name: + type: string + required: + - name + type: array download: type: string elasticsearch: @@ -24309,6 +24354,7 @@ paths: enum: - integration - input + - content type: string vars: items: @@ -24398,6 +24444,20 @@ paths: type: array description: type: string + discovery: + additionalProperties: true + type: object + properties: + fields: + items: + additionalProperties: true + type: object + properties: + name: + type: string + required: + - name + type: array download: type: string elasticsearch: @@ -24705,6 +24765,7 @@ paths: enum: - integration - input + - content type: string vars: items: @@ -25057,6 +25118,20 @@ paths: type: array description: type: string + discovery: + additionalProperties: true + type: object + properties: + fields: + items: + additionalProperties: true + type: object + properties: + name: + type: string + required: + - name + type: array download: type: string elasticsearch: @@ -25364,6 +25439,7 @@ paths: enum: - integration - input + - content type: string vars: items: @@ -25445,6 +25521,20 @@ paths: type: array description: type: string + discovery: + additionalProperties: true + type: object + properties: + fields: + items: + additionalProperties: true + type: object + properties: + name: + type: string + required: + - name + type: array download: type: string elasticsearch: @@ -25752,6 +25842,7 @@ paths: enum: - integration - input + - content type: string vars: items: @@ -49361,6 +49452,7 @@ components: enum: - OpenAI - Azure OpenAI + - Other type: string Security_AI_Assistant_API_Reader: additionalProperties: true @@ -50006,10 +50098,6 @@ components: $ref: '#/components/schemas/Security_Detections_API_RuleFilterArray' index: $ref: '#/components/schemas/Security_Detections_API_IndexPatternArray' - response_actions: - items: - $ref: '#/components/schemas/Security_Detections_API_ResponseAction' - type: array tiebreaker_field: $ref: '#/components/schemas/Security_Detections_API_TiebreakerField' timestamp_field: @@ -50099,6 +50187,10 @@ components: $ref: >- #/components/schemas/Security_Detections_API_RequiredFieldInput type: array + response_actions: + items: + $ref: '#/components/schemas/Security_Detections_API_ResponseAction' + type: array risk_score: $ref: '#/components/schemas/Security_Detections_API_RiskScore' risk_score_mapping: @@ -50223,6 +50315,10 @@ components: $ref: >- #/components/schemas/Security_Detections_API_RequiredFieldInput type: array + response_actions: + items: + $ref: '#/components/schemas/Security_Detections_API_ResponseAction' + type: array risk_score: $ref: '#/components/schemas/Security_Detections_API_RiskScore' risk_score_mapping: @@ -50344,6 +50440,10 @@ components: $ref: >- #/components/schemas/Security_Detections_API_RequiredFieldInput type: array + response_actions: + items: + $ref: '#/components/schemas/Security_Detections_API_ResponseAction' + type: array risk_score: $ref: '#/components/schemas/Security_Detections_API_RiskScore' risk_score_mapping: @@ -50448,6 +50548,10 @@ components: $ref: >- #/components/schemas/Security_Detections_API_RequiredFieldInput type: array + response_actions: + items: + $ref: '#/components/schemas/Security_Detections_API_ResponseAction' + type: array risk_score: $ref: '#/components/schemas/Security_Detections_API_RiskScore' risk_score_mapping: @@ -50582,6 +50686,10 @@ components: $ref: >- #/components/schemas/Security_Detections_API_RequiredFieldInput type: array + response_actions: + items: + $ref: '#/components/schemas/Security_Detections_API_ResponseAction' + type: array risk_score: $ref: '#/components/schemas/Security_Detections_API_RiskScore' risk_score_mapping: @@ -50706,6 +50814,10 @@ components: $ref: >- #/components/schemas/Security_Detections_API_RequiredFieldInput type: array + response_actions: + items: + $ref: '#/components/schemas/Security_Detections_API_ResponseAction' + type: array risk_score: $ref: '#/components/schemas/Security_Detections_API_RiskScore' risk_score_mapping: @@ -50751,10 +50863,6 @@ components: properties: alert_suppression: $ref: '#/components/schemas/Security_Detections_API_AlertSuppression' - response_actions: - items: - $ref: '#/components/schemas/Security_Detections_API_ResponseAction' - type: array Security_Detections_API_EsqlRulePatchProps: allOf: - type: object @@ -50825,6 +50933,10 @@ components: $ref: >- #/components/schemas/Security_Detections_API_RequiredFieldInput type: array + response_actions: + items: + $ref: '#/components/schemas/Security_Detections_API_ResponseAction' + type: array risk_score: $ref: '#/components/schemas/Security_Detections_API_RiskScore' risk_score_mapping: @@ -50951,6 +51063,10 @@ components: $ref: >- #/components/schemas/Security_Detections_API_RequiredFieldInput type: array + response_actions: + items: + $ref: '#/components/schemas/Security_Detections_API_ResponseAction' + type: array risk_score: $ref: '#/components/schemas/Security_Detections_API_RiskScore' risk_score_mapping: @@ -51213,6 +51329,10 @@ components: $ref: >- #/components/schemas/Security_Detections_API_RequiredFieldInput type: array + response_actions: + items: + $ref: '#/components/schemas/Security_Detections_API_ResponseAction' + type: array risk_score: $ref: '#/components/schemas/Security_Detections_API_RiskScore' risk_score_mapping: @@ -51340,6 +51460,10 @@ components: $ref: >- #/components/schemas/Security_Detections_API_RequiredFieldInput type: array + response_actions: + items: + $ref: '#/components/schemas/Security_Detections_API_ResponseAction' + type: array risk_score: $ref: '#/components/schemas/Security_Detections_API_RiskScore' risk_score_mapping: @@ -51467,6 +51591,10 @@ components: $ref: >- #/components/schemas/Security_Detections_API_RequiredFieldInput type: array + response_actions: + items: + $ref: '#/components/schemas/Security_Detections_API_ResponseAction' + type: array risk_score: $ref: '#/components/schemas/Security_Detections_API_RiskScore' risk_score_mapping: @@ -51590,6 +51718,10 @@ components: $ref: >- #/components/schemas/Security_Detections_API_RequiredFieldInput type: array + response_actions: + items: + $ref: '#/components/schemas/Security_Detections_API_ResponseAction' + type: array risk_score: $ref: '#/components/schemas/Security_Detections_API_RiskScore' risk_score_mapping: @@ -51801,6 +51933,10 @@ components: $ref: >- #/components/schemas/Security_Detections_API_RequiredFieldInput type: array + response_actions: + items: + $ref: '#/components/schemas/Security_Detections_API_ResponseAction' + type: array risk_score: $ref: '#/components/schemas/Security_Detections_API_RiskScore' risk_score_mapping: @@ -51930,6 +52066,10 @@ components: $ref: >- #/components/schemas/Security_Detections_API_RequiredFieldInput type: array + response_actions: + items: + $ref: '#/components/schemas/Security_Detections_API_ResponseAction' + type: array risk_score: $ref: '#/components/schemas/Security_Detections_API_RiskScore' risk_score_mapping: @@ -51987,10 +52127,6 @@ components: $ref: '#/components/schemas/Security_Detections_API_RuleFilterArray' index: $ref: '#/components/schemas/Security_Detections_API_IndexPatternArray' - response_actions: - items: - $ref: '#/components/schemas/Security_Detections_API_ResponseAction' - type: array Security_Detections_API_NewTermsRulePatchFields: allOf: - type: object @@ -52075,6 +52211,10 @@ components: $ref: >- #/components/schemas/Security_Detections_API_RequiredFieldInput type: array + response_actions: + items: + $ref: '#/components/schemas/Security_Detections_API_ResponseAction' + type: array risk_score: $ref: '#/components/schemas/Security_Detections_API_RiskScore' risk_score_mapping: @@ -52206,6 +52346,10 @@ components: $ref: >- #/components/schemas/Security_Detections_API_RequiredFieldInput type: array + response_actions: + items: + $ref: '#/components/schemas/Security_Detections_API_ResponseAction' + type: array risk_score: $ref: '#/components/schemas/Security_Detections_API_RiskScore' risk_score_mapping: @@ -52440,6 +52584,10 @@ components: $ref: >- #/components/schemas/Security_Detections_API_RequiredFieldInput type: array + response_actions: + items: + $ref: '#/components/schemas/Security_Detections_API_ResponseAction' + type: array risk_score: $ref: '#/components/schemas/Security_Detections_API_RiskScore' risk_score_mapping: @@ -52566,6 +52714,10 @@ components: $ref: >- #/components/schemas/Security_Detections_API_RequiredFieldInput type: array + response_actions: + items: + $ref: '#/components/schemas/Security_Detections_API_ResponseAction' + type: array risk_score: $ref: '#/components/schemas/Security_Detections_API_RiskScore' risk_score_mapping: @@ -52624,10 +52776,6 @@ components: $ref: '#/components/schemas/Security_Detections_API_RuleFilterArray' index: $ref: '#/components/schemas/Security_Detections_API_IndexPatternArray' - response_actions: - items: - $ref: '#/components/schemas/Security_Detections_API_ResponseAction' - type: array saved_id: $ref: '#/components/schemas/Security_Detections_API_SavedQueryId' Security_Detections_API_QueryRulePatchFields: @@ -52707,6 +52855,10 @@ components: $ref: >- #/components/schemas/Security_Detections_API_RequiredFieldInput type: array + response_actions: + items: + $ref: '#/components/schemas/Security_Detections_API_ResponseAction' + type: array risk_score: $ref: '#/components/schemas/Security_Detections_API_RiskScore' risk_score_mapping: @@ -52830,6 +52982,10 @@ components: $ref: >- #/components/schemas/Security_Detections_API_RequiredFieldInput type: array + response_actions: + items: + $ref: '#/components/schemas/Security_Detections_API_ResponseAction' + type: array risk_score: $ref: '#/components/schemas/Security_Detections_API_RiskScore' risk_score_mapping: @@ -53539,6 +53695,10 @@ components: $ref: >- #/components/schemas/Security_Detections_API_RequiredFieldInput type: array + response_actions: + items: + $ref: '#/components/schemas/Security_Detections_API_ResponseAction' + type: array risk_score: $ref: '#/components/schemas/Security_Detections_API_RiskScore' risk_score_mapping: @@ -53668,6 +53828,10 @@ components: $ref: >- #/components/schemas/Security_Detections_API_RequiredFieldInput type: array + response_actions: + items: + $ref: '#/components/schemas/Security_Detections_API_ResponseAction' + type: array risk_score: $ref: '#/components/schemas/Security_Detections_API_RiskScore' risk_score_mapping: @@ -53727,10 +53891,6 @@ components: $ref: '#/components/schemas/Security_Detections_API_IndexPatternArray' query: $ref: '#/components/schemas/Security_Detections_API_RuleQuery' - response_actions: - items: - $ref: '#/components/schemas/Security_Detections_API_ResponseAction' - type: array Security_Detections_API_SavedQueryRulePatchFields: allOf: - type: object @@ -53811,6 +53971,10 @@ components: $ref: >- #/components/schemas/Security_Detections_API_RequiredFieldInput type: array + response_actions: + items: + $ref: '#/components/schemas/Security_Detections_API_ResponseAction' + type: array risk_score: $ref: '#/components/schemas/Security_Detections_API_RiskScore' risk_score_mapping: @@ -53937,6 +54101,10 @@ components: $ref: >- #/components/schemas/Security_Detections_API_RequiredFieldInput type: array + response_actions: + items: + $ref: '#/components/schemas/Security_Detections_API_ResponseAction' + type: array risk_score: $ref: '#/components/schemas/Security_Detections_API_RiskScore' risk_score_mapping: @@ -54194,6 +54362,10 @@ components: $ref: >- #/components/schemas/Security_Detections_API_RequiredFieldInput type: array + response_actions: + items: + $ref: '#/components/schemas/Security_Detections_API_ResponseAction' + type: array risk_score: $ref: '#/components/schemas/Security_Detections_API_RiskScore' risk_score_mapping: @@ -54323,6 +54495,10 @@ components: $ref: >- #/components/schemas/Security_Detections_API_RequiredFieldInput type: array + response_actions: + items: + $ref: '#/components/schemas/Security_Detections_API_ResponseAction' + type: array risk_score: $ref: '#/components/schemas/Security_Detections_API_RiskScore' risk_score_mapping: @@ -54478,6 +54654,10 @@ components: $ref: >- #/components/schemas/Security_Detections_API_RequiredFieldInput type: array + response_actions: + items: + $ref: '#/components/schemas/Security_Detections_API_ResponseAction' + type: array risk_score: $ref: '#/components/schemas/Security_Detections_API_RiskScore' risk_score_mapping: @@ -54613,6 +54793,10 @@ components: $ref: >- #/components/schemas/Security_Detections_API_RequiredFieldInput type: array + response_actions: + items: + $ref: '#/components/schemas/Security_Detections_API_ResponseAction' + type: array risk_score: $ref: '#/components/schemas/Security_Detections_API_RiskScore' risk_score_mapping: @@ -54813,6 +54997,10 @@ components: $ref: >- #/components/schemas/Security_Detections_API_RequiredFieldInput type: array + response_actions: + items: + $ref: '#/components/schemas/Security_Detections_API_ResponseAction' + type: array risk_score: $ref: '#/components/schemas/Security_Detections_API_RiskScore' risk_score_mapping: @@ -54942,6 +55130,10 @@ components: $ref: >- #/components/schemas/Security_Detections_API_RequiredFieldInput type: array + response_actions: + items: + $ref: '#/components/schemas/Security_Detections_API_ResponseAction' + type: array risk_score: $ref: '#/components/schemas/Security_Detections_API_RiskScore' risk_score_mapping: @@ -55084,6 +55276,10 @@ components: $ref: >- #/components/schemas/Security_Detections_API_RequiredFieldInput type: array + response_actions: + items: + $ref: '#/components/schemas/Security_Detections_API_ResponseAction' + type: array risk_score: $ref: '#/components/schemas/Security_Detections_API_RiskScore' risk_score_mapping: @@ -55213,6 +55409,10 @@ components: $ref: >- #/components/schemas/Security_Detections_API_RequiredFieldInput type: array + response_actions: + items: + $ref: '#/components/schemas/Security_Detections_API_ResponseAction' + type: array risk_score: $ref: '#/components/schemas/Security_Detections_API_RiskScore' risk_score_mapping: diff --git a/oas_docs/output/kibana.yaml b/oas_docs/output/kibana.yaml index 372569024e114..6aa75efa5bd70 100644 --- a/oas_docs/output/kibana.yaml +++ b/oas_docs/output/kibana.yaml @@ -11452,6 +11452,7 @@ paths: - Security Endpoint Management API /api/endpoint/metadata/transforms: get: + deprecated: true operationId: GetEndpointMetadataTransform responses: '200': @@ -22418,6 +22419,20 @@ paths: type: array description: type: string + discovery: + additionalProperties: true + type: object + properties: + fields: + items: + additionalProperties: true + type: object + properties: + name: + type: string + required: + - name + type: array download: type: string format_version: @@ -22698,6 +22713,7 @@ paths: enum: - integration - input + - content type: string vars: items: @@ -22750,6 +22766,20 @@ paths: type: array description: type: string + discovery: + additionalProperties: true + type: object + properties: + fields: + items: + additionalProperties: true + type: object + properties: + name: + type: string + required: + - name + type: array download: type: string format_version: @@ -23030,6 +23060,7 @@ paths: enum: - integration - input + - content type: string vars: items: @@ -24002,6 +24033,20 @@ paths: type: array description: type: string + discovery: + additionalProperties: true + type: object + properties: + fields: + items: + additionalProperties: true + type: object + properties: + name: + type: string + required: + - name + type: array download: type: string elasticsearch: @@ -24309,6 +24354,7 @@ paths: enum: - integration - input + - content type: string vars: items: @@ -24398,6 +24444,20 @@ paths: type: array description: type: string + discovery: + additionalProperties: true + type: object + properties: + fields: + items: + additionalProperties: true + type: object + properties: + name: + type: string + required: + - name + type: array download: type: string elasticsearch: @@ -24705,6 +24765,7 @@ paths: enum: - integration - input + - content type: string vars: items: @@ -25057,6 +25118,20 @@ paths: type: array description: type: string + discovery: + additionalProperties: true + type: object + properties: + fields: + items: + additionalProperties: true + type: object + properties: + name: + type: string + required: + - name + type: array download: type: string elasticsearch: @@ -25364,6 +25439,7 @@ paths: enum: - integration - input + - content type: string vars: items: @@ -25445,6 +25521,20 @@ paths: type: array description: type: string + discovery: + additionalProperties: true + type: object + properties: + fields: + items: + additionalProperties: true + type: object + properties: + name: + type: string + required: + - name + type: array download: type: string elasticsearch: @@ -25752,6 +25842,7 @@ paths: enum: - integration - input + - content type: string vars: items: @@ -49361,6 +49452,7 @@ components: enum: - OpenAI - Azure OpenAI + - Other type: string Security_AI_Assistant_API_Reader: additionalProperties: true @@ -50006,10 +50098,6 @@ components: $ref: '#/components/schemas/Security_Detections_API_RuleFilterArray' index: $ref: '#/components/schemas/Security_Detections_API_IndexPatternArray' - response_actions: - items: - $ref: '#/components/schemas/Security_Detections_API_ResponseAction' - type: array tiebreaker_field: $ref: '#/components/schemas/Security_Detections_API_TiebreakerField' timestamp_field: @@ -50099,6 +50187,10 @@ components: $ref: >- #/components/schemas/Security_Detections_API_RequiredFieldInput type: array + response_actions: + items: + $ref: '#/components/schemas/Security_Detections_API_ResponseAction' + type: array risk_score: $ref: '#/components/schemas/Security_Detections_API_RiskScore' risk_score_mapping: @@ -50223,6 +50315,10 @@ components: $ref: >- #/components/schemas/Security_Detections_API_RequiredFieldInput type: array + response_actions: + items: + $ref: '#/components/schemas/Security_Detections_API_ResponseAction' + type: array risk_score: $ref: '#/components/schemas/Security_Detections_API_RiskScore' risk_score_mapping: @@ -50344,6 +50440,10 @@ components: $ref: >- #/components/schemas/Security_Detections_API_RequiredFieldInput type: array + response_actions: + items: + $ref: '#/components/schemas/Security_Detections_API_ResponseAction' + type: array risk_score: $ref: '#/components/schemas/Security_Detections_API_RiskScore' risk_score_mapping: @@ -50448,6 +50548,10 @@ components: $ref: >- #/components/schemas/Security_Detections_API_RequiredFieldInput type: array + response_actions: + items: + $ref: '#/components/schemas/Security_Detections_API_ResponseAction' + type: array risk_score: $ref: '#/components/schemas/Security_Detections_API_RiskScore' risk_score_mapping: @@ -50582,6 +50686,10 @@ components: $ref: >- #/components/schemas/Security_Detections_API_RequiredFieldInput type: array + response_actions: + items: + $ref: '#/components/schemas/Security_Detections_API_ResponseAction' + type: array risk_score: $ref: '#/components/schemas/Security_Detections_API_RiskScore' risk_score_mapping: @@ -50706,6 +50814,10 @@ components: $ref: >- #/components/schemas/Security_Detections_API_RequiredFieldInput type: array + response_actions: + items: + $ref: '#/components/schemas/Security_Detections_API_ResponseAction' + type: array risk_score: $ref: '#/components/schemas/Security_Detections_API_RiskScore' risk_score_mapping: @@ -50751,10 +50863,6 @@ components: properties: alert_suppression: $ref: '#/components/schemas/Security_Detections_API_AlertSuppression' - response_actions: - items: - $ref: '#/components/schemas/Security_Detections_API_ResponseAction' - type: array Security_Detections_API_EsqlRulePatchProps: allOf: - type: object @@ -50825,6 +50933,10 @@ components: $ref: >- #/components/schemas/Security_Detections_API_RequiredFieldInput type: array + response_actions: + items: + $ref: '#/components/schemas/Security_Detections_API_ResponseAction' + type: array risk_score: $ref: '#/components/schemas/Security_Detections_API_RiskScore' risk_score_mapping: @@ -50951,6 +51063,10 @@ components: $ref: >- #/components/schemas/Security_Detections_API_RequiredFieldInput type: array + response_actions: + items: + $ref: '#/components/schemas/Security_Detections_API_ResponseAction' + type: array risk_score: $ref: '#/components/schemas/Security_Detections_API_RiskScore' risk_score_mapping: @@ -51213,6 +51329,10 @@ components: $ref: >- #/components/schemas/Security_Detections_API_RequiredFieldInput type: array + response_actions: + items: + $ref: '#/components/schemas/Security_Detections_API_ResponseAction' + type: array risk_score: $ref: '#/components/schemas/Security_Detections_API_RiskScore' risk_score_mapping: @@ -51340,6 +51460,10 @@ components: $ref: >- #/components/schemas/Security_Detections_API_RequiredFieldInput type: array + response_actions: + items: + $ref: '#/components/schemas/Security_Detections_API_ResponseAction' + type: array risk_score: $ref: '#/components/schemas/Security_Detections_API_RiskScore' risk_score_mapping: @@ -51467,6 +51591,10 @@ components: $ref: >- #/components/schemas/Security_Detections_API_RequiredFieldInput type: array + response_actions: + items: + $ref: '#/components/schemas/Security_Detections_API_ResponseAction' + type: array risk_score: $ref: '#/components/schemas/Security_Detections_API_RiskScore' risk_score_mapping: @@ -51590,6 +51718,10 @@ components: $ref: >- #/components/schemas/Security_Detections_API_RequiredFieldInput type: array + response_actions: + items: + $ref: '#/components/schemas/Security_Detections_API_ResponseAction' + type: array risk_score: $ref: '#/components/schemas/Security_Detections_API_RiskScore' risk_score_mapping: @@ -51801,6 +51933,10 @@ components: $ref: >- #/components/schemas/Security_Detections_API_RequiredFieldInput type: array + response_actions: + items: + $ref: '#/components/schemas/Security_Detections_API_ResponseAction' + type: array risk_score: $ref: '#/components/schemas/Security_Detections_API_RiskScore' risk_score_mapping: @@ -51930,6 +52066,10 @@ components: $ref: >- #/components/schemas/Security_Detections_API_RequiredFieldInput type: array + response_actions: + items: + $ref: '#/components/schemas/Security_Detections_API_ResponseAction' + type: array risk_score: $ref: '#/components/schemas/Security_Detections_API_RiskScore' risk_score_mapping: @@ -51987,10 +52127,6 @@ components: $ref: '#/components/schemas/Security_Detections_API_RuleFilterArray' index: $ref: '#/components/schemas/Security_Detections_API_IndexPatternArray' - response_actions: - items: - $ref: '#/components/schemas/Security_Detections_API_ResponseAction' - type: array Security_Detections_API_NewTermsRulePatchFields: allOf: - type: object @@ -52075,6 +52211,10 @@ components: $ref: >- #/components/schemas/Security_Detections_API_RequiredFieldInput type: array + response_actions: + items: + $ref: '#/components/schemas/Security_Detections_API_ResponseAction' + type: array risk_score: $ref: '#/components/schemas/Security_Detections_API_RiskScore' risk_score_mapping: @@ -52206,6 +52346,10 @@ components: $ref: >- #/components/schemas/Security_Detections_API_RequiredFieldInput type: array + response_actions: + items: + $ref: '#/components/schemas/Security_Detections_API_ResponseAction' + type: array risk_score: $ref: '#/components/schemas/Security_Detections_API_RiskScore' risk_score_mapping: @@ -52440,6 +52584,10 @@ components: $ref: >- #/components/schemas/Security_Detections_API_RequiredFieldInput type: array + response_actions: + items: + $ref: '#/components/schemas/Security_Detections_API_ResponseAction' + type: array risk_score: $ref: '#/components/schemas/Security_Detections_API_RiskScore' risk_score_mapping: @@ -52566,6 +52714,10 @@ components: $ref: >- #/components/schemas/Security_Detections_API_RequiredFieldInput type: array + response_actions: + items: + $ref: '#/components/schemas/Security_Detections_API_ResponseAction' + type: array risk_score: $ref: '#/components/schemas/Security_Detections_API_RiskScore' risk_score_mapping: @@ -52624,10 +52776,6 @@ components: $ref: '#/components/schemas/Security_Detections_API_RuleFilterArray' index: $ref: '#/components/schemas/Security_Detections_API_IndexPatternArray' - response_actions: - items: - $ref: '#/components/schemas/Security_Detections_API_ResponseAction' - type: array saved_id: $ref: '#/components/schemas/Security_Detections_API_SavedQueryId' Security_Detections_API_QueryRulePatchFields: @@ -52707,6 +52855,10 @@ components: $ref: >- #/components/schemas/Security_Detections_API_RequiredFieldInput type: array + response_actions: + items: + $ref: '#/components/schemas/Security_Detections_API_ResponseAction' + type: array risk_score: $ref: '#/components/schemas/Security_Detections_API_RiskScore' risk_score_mapping: @@ -52830,6 +52982,10 @@ components: $ref: >- #/components/schemas/Security_Detections_API_RequiredFieldInput type: array + response_actions: + items: + $ref: '#/components/schemas/Security_Detections_API_ResponseAction' + type: array risk_score: $ref: '#/components/schemas/Security_Detections_API_RiskScore' risk_score_mapping: @@ -53539,6 +53695,10 @@ components: $ref: >- #/components/schemas/Security_Detections_API_RequiredFieldInput type: array + response_actions: + items: + $ref: '#/components/schemas/Security_Detections_API_ResponseAction' + type: array risk_score: $ref: '#/components/schemas/Security_Detections_API_RiskScore' risk_score_mapping: @@ -53668,6 +53828,10 @@ components: $ref: >- #/components/schemas/Security_Detections_API_RequiredFieldInput type: array + response_actions: + items: + $ref: '#/components/schemas/Security_Detections_API_ResponseAction' + type: array risk_score: $ref: '#/components/schemas/Security_Detections_API_RiskScore' risk_score_mapping: @@ -53727,10 +53891,6 @@ components: $ref: '#/components/schemas/Security_Detections_API_IndexPatternArray' query: $ref: '#/components/schemas/Security_Detections_API_RuleQuery' - response_actions: - items: - $ref: '#/components/schemas/Security_Detections_API_ResponseAction' - type: array Security_Detections_API_SavedQueryRulePatchFields: allOf: - type: object @@ -53811,6 +53971,10 @@ components: $ref: >- #/components/schemas/Security_Detections_API_RequiredFieldInput type: array + response_actions: + items: + $ref: '#/components/schemas/Security_Detections_API_ResponseAction' + type: array risk_score: $ref: '#/components/schemas/Security_Detections_API_RiskScore' risk_score_mapping: @@ -53937,6 +54101,10 @@ components: $ref: >- #/components/schemas/Security_Detections_API_RequiredFieldInput type: array + response_actions: + items: + $ref: '#/components/schemas/Security_Detections_API_ResponseAction' + type: array risk_score: $ref: '#/components/schemas/Security_Detections_API_RiskScore' risk_score_mapping: @@ -54194,6 +54362,10 @@ components: $ref: >- #/components/schemas/Security_Detections_API_RequiredFieldInput type: array + response_actions: + items: + $ref: '#/components/schemas/Security_Detections_API_ResponseAction' + type: array risk_score: $ref: '#/components/schemas/Security_Detections_API_RiskScore' risk_score_mapping: @@ -54323,6 +54495,10 @@ components: $ref: >- #/components/schemas/Security_Detections_API_RequiredFieldInput type: array + response_actions: + items: + $ref: '#/components/schemas/Security_Detections_API_ResponseAction' + type: array risk_score: $ref: '#/components/schemas/Security_Detections_API_RiskScore' risk_score_mapping: @@ -54478,6 +54654,10 @@ components: $ref: >- #/components/schemas/Security_Detections_API_RequiredFieldInput type: array + response_actions: + items: + $ref: '#/components/schemas/Security_Detections_API_ResponseAction' + type: array risk_score: $ref: '#/components/schemas/Security_Detections_API_RiskScore' risk_score_mapping: @@ -54613,6 +54793,10 @@ components: $ref: >- #/components/schemas/Security_Detections_API_RequiredFieldInput type: array + response_actions: + items: + $ref: '#/components/schemas/Security_Detections_API_ResponseAction' + type: array risk_score: $ref: '#/components/schemas/Security_Detections_API_RiskScore' risk_score_mapping: @@ -54813,6 +54997,10 @@ components: $ref: >- #/components/schemas/Security_Detections_API_RequiredFieldInput type: array + response_actions: + items: + $ref: '#/components/schemas/Security_Detections_API_ResponseAction' + type: array risk_score: $ref: '#/components/schemas/Security_Detections_API_RiskScore' risk_score_mapping: @@ -54942,6 +55130,10 @@ components: $ref: >- #/components/schemas/Security_Detections_API_RequiredFieldInput type: array + response_actions: + items: + $ref: '#/components/schemas/Security_Detections_API_ResponseAction' + type: array risk_score: $ref: '#/components/schemas/Security_Detections_API_RiskScore' risk_score_mapping: @@ -55084,6 +55276,10 @@ components: $ref: >- #/components/schemas/Security_Detections_API_RequiredFieldInput type: array + response_actions: + items: + $ref: '#/components/schemas/Security_Detections_API_ResponseAction' + type: array risk_score: $ref: '#/components/schemas/Security_Detections_API_RiskScore' risk_score_mapping: @@ -55213,6 +55409,10 @@ components: $ref: >- #/components/schemas/Security_Detections_API_RequiredFieldInput type: array + response_actions: + items: + $ref: '#/components/schemas/Security_Detections_API_ResponseAction' + type: array risk_score: $ref: '#/components/schemas/Security_Detections_API_RiskScore' risk_score_mapping: diff --git a/package.json b/package.json index 02b7f61924abb..734ce9cce5128 100644 --- a/package.json +++ b/package.json @@ -97,6 +97,7 @@ "@storybook/react-docgen-typescript-plugin": "1.0.6--canary.9.cd77847.0", "@types/react": "~18.2.0", "@types/react-dom": "~18.2.0", + "@xstate5/react/**/xstate": "^5.18.1", "globby/fast-glob": "^3.2.11" }, "dependencies": { @@ -111,7 +112,7 @@ "@elastic/apm-rum": "^5.16.1", "@elastic/apm-rum-core": "^5.21.1", "@elastic/apm-rum-react": "^2.0.3", - "@elastic/charts": "67.0.1", + "@elastic/charts": "68.0.0", "@elastic/datemath": "5.0.3", "@elastic/ebt": "^1.1.1", "@elastic/ecs": "^8.11.1", @@ -153,11 +154,11 @@ "@hapi/wreck": "^18.1.0", "@hello-pangea/dnd": "16.6.0", "@kbn/aad-fixtures-plugin": "link:x-pack/test/alerting_api_integration/common/plugins/aad", - "@kbn/ace": "link:packages/kbn-ace", "@kbn/actions-plugin": "link:x-pack/plugins/actions", "@kbn/actions-simulators-plugin": "link:x-pack/test/alerting_api_integration/common/plugins/actions_simulators", "@kbn/actions-types": "link:packages/kbn-actions-types", "@kbn/advanced-settings-plugin": "link:src/plugins/advanced_settings", + "@kbn/ai-assistant": "link:x-pack/packages/kbn-ai-assistant", "@kbn/ai-assistant-management-plugin": "link:src/plugins/ai_assistant_management/selection", "@kbn/aiops-change-point-detection": "link:x-pack/packages/ml/aiops_change_point_detection", "@kbn/aiops-common": "link:x-pack/packages/ml/aiops_common", @@ -687,6 +688,7 @@ "@kbn/observability-fixtures-plugin": "link:x-pack/test/cases_api_integration/common/plugins/observability", "@kbn/observability-get-padded-alert-time-range-util": "link:x-pack/packages/observability/get_padded_alert_time_range_util", "@kbn/observability-logs-explorer-plugin": "link:x-pack/plugins/observability_solution/observability_logs_explorer", + "@kbn/observability-logs-overview": "link:x-pack/packages/observability/logs_overview", "@kbn/observability-onboarding-plugin": "link:x-pack/plugins/observability_solution/observability_onboarding", "@kbn/observability-plugin": "link:x-pack/plugins/observability_solution/observability", "@kbn/observability-shared-plugin": "link:x-pack/plugins/observability_solution/observability_shared", @@ -1050,6 +1052,7 @@ "@turf/helpers": "6.0.1", "@turf/length": "^6.0.2", "@xstate/react": "^3.2.2", + "@xstate5/react": "npm:@xstate/react@^4.1.2", "adm-zip": "^0.5.9", "ai": "^2.2.33", "ajv": "^8.12.0", @@ -1063,7 +1066,6 @@ "bitmap-sdf": "^1.0.3", "blurhash": "^2.0.1", "borc": "3.0.0", - "brace": "0.11.1", "brok": "^6.0.0", "byte-size": "^8.1.0", "cacheable-lookup": "6", @@ -1099,7 +1101,7 @@ "del": "^6.1.0", "diff": "^5.1.0", "dotenv": "^16.4.5", - "elastic-apm-node": "^4.7.3", + "elastic-apm-node": "^4.8.0", "email-addresses": "^5.0.0", "eventsource-parser": "^1.1.1", "execa": "^5.1.1", @@ -1201,7 +1203,6 @@ "re-resizable": "^6.9.9", "re2js": "0.4.2", "react": "^17.0.2", - "react-ace": "^7.0.5", "react-diff-view": "^3.2.1", "react-dom": "^17.0.2", "react-dropzone": "^4.2.9", @@ -1283,6 +1284,7 @@ "whatwg-fetch": "^3.0.0", "xml2js": "^0.5.0", "xstate": "^4.38.2", + "xstate5": "npm:xstate@^5.18.1", "xterm": "^5.1.0", "yauzl": "^2.10.0", "yazl": "^2.5.1", @@ -1304,6 +1306,7 @@ "@babel/plugin-proposal-optional-chaining": "^7.21.0", "@babel/plugin-proposal-private-methods": "^7.18.6", "@babel/plugin-transform-class-properties": "^7.24.7", + "@babel/plugin-transform-logical-assignment-operators": "^7.24.7", "@babel/plugin-transform-numeric-separator": "^7.24.7", "@babel/plugin-transform-runtime": "^7.24.7", "@babel/preset-env": "^7.24.7", diff --git a/packages/core/application/core-application-browser/src/app_mount.ts b/packages/core/application/core-application-browser/src/app_mount.ts index a34550bc98fcd..4fb38b10a3704 100644 --- a/packages/core/application/core-application-browser/src/app_mount.ts +++ b/packages/core/application/core-application-browser/src/app_mount.ts @@ -89,7 +89,7 @@ export interface AppMountParameters { * This string should not include the base path from HTTP. * * @deprecated Use {@link AppMountParameters.history} instead. - * @removeBy 8.8.0 + * remove after https://github.com/elastic/kibana/issues/132600 is done * * @example * diff --git a/packages/core/elasticsearch/core-elasticsearch-server/src/contracts.ts b/packages/core/elasticsearch/core-elasticsearch-server/src/contracts.ts index bc712a61a535e..4e0bd253eb8b4 100644 --- a/packages/core/elasticsearch/core-elasticsearch-server/src/contracts.ts +++ b/packages/core/elasticsearch/core-elasticsearch-server/src/contracts.ts @@ -81,7 +81,7 @@ export interface ElasticsearchServiceSetup { setUnauthorizedErrorHandler: (handler: UnauthorizedErrorHandler) => void; /** - * @deprecated + * @deprecated Can be removed when https://github.com/elastic/kibana/issues/119862 is done. */ legacy: { /** diff --git a/packages/core/http/core-http-resources-server-internal/src/http_resources_service.test.ts b/packages/core/http/core-http-resources-server-internal/src/http_resources_service.test.ts index efce905e6564f..1a7757d4e1eaa 100644 --- a/packages/core/http/core-http-resources-server-internal/src/http_resources_service.test.ts +++ b/packages/core/http/core-http-resources-server-internal/src/http_resources_service.test.ts @@ -69,6 +69,23 @@ describe('HttpResources service', () => { expect(registeredRouteConfig.options?.access).toBe('internal'); }); + it('registration defaults to excluded from OAS', () => { + register({ ...routeConfig, options: { access: 'internal' } }, async (ctx, req, res) => + res.ok() + ); + const [[registeredRouteConfig]] = router.get.mock.calls; + expect(registeredRouteConfig.options?.excludeFromOAS).toBe(true); + }); + + it('registration allows being included in OAS', () => { + register( + { ...routeConfig, options: { access: 'internal', excludeFromOAS: false } }, + async (ctx, req, res) => res.ok() + ); + const [[registeredRouteConfig]] = router.get.mock.calls; + expect(registeredRouteConfig.options?.excludeFromOAS).toBe(false); + }); + describe('renderCoreApp', () => { it('formats successful response', async () => { register(routeConfig, async (ctx, req, res) => { diff --git a/packages/core/http/core-http-resources-server-internal/src/http_resources_service.ts b/packages/core/http/core-http-resources-server-internal/src/http_resources_service.ts index d9e75d49e72cf..29114c0dffc07 100644 --- a/packages/core/http/core-http-resources-server-internal/src/http_resources_service.ts +++ b/packages/core/http/core-http-resources-server-internal/src/http_resources_service.ts @@ -89,6 +89,7 @@ export class HttpResourcesService implements CoreService mockResponse), header: jest.fn().mockImplementation(() => mockResponse), -}; -const mockResponseToolkit: any = { +} as unknown as jest.Mocked; + +const mockResponseToolkit = { response: jest.fn().mockReturnValue(mockResponse), -}; +} as unknown as jest.Mocked; const logger = loggingSystemMock.create().get(); const enhanceWithContext = (fn: (...args: any[]) => any) => fn.bind(null, {}); @@ -132,6 +134,42 @@ describe('Router', () => { } ); + it('adds versioned header v2023-10-31 to public, unversioned routes', async () => { + const router = new Router('', logger, enhanceWithContext, routerOptions); + router.post( + { + path: '/public', + options: { + access: 'public', + }, + validate: false, + }, + (context, req, res) => res.ok({ headers: { AAAA: 'test' } }) // with some fake headers + ); + router.post( + { + path: '/internal', + options: { + access: 'internal', + }, + validate: false, + }, + (context, req, res) => res.ok() + ); + const [{ handler: publicHandler }, { handler: internalHandler }] = router.getRoutes(); + + await publicHandler(createRequestMock(), mockResponseToolkit); + expect(mockResponse.header).toHaveBeenCalledTimes(2); + const [first, second] = mockResponse.header.mock.calls + .concat() + .sort(([k1], [k2]) => k1.localeCompare(k2)); + expect(first).toEqual(['AAAA', 'test']); + expect(second).toEqual(['elastic-api-version', '2023-10-31']); + + await internalHandler(createRequestMock(), mockResponseToolkit); + expect(mockResponse.header).toHaveBeenCalledTimes(2); // no additional calls + }); + it('constructs lazily provided validations once (idempotency)', async () => { const router = new Router('', logger, enhanceWithContext, routerOptions); const { fooValidation } = testValidation; diff --git a/packages/core/http/core-http-router-server-internal/src/router.ts b/packages/core/http/core-http-router-server-internal/src/router.ts index 52363e7ea95be..bb99de64581be 100644 --- a/packages/core/http/core-http-router-server-internal/src/router.ts +++ b/packages/core/http/core-http-router-server-internal/src/router.ts @@ -33,13 +33,13 @@ import { validBodyOutput, getRequestValidation } from '@kbn/core-http-server'; import type { RouteSecurityGetter } from '@kbn/core-http-server'; import type { DeepPartial } from '@kbn/utility-types'; import { RouteValidator } from './validator'; -import { CoreVersionedRouter } from './versioned_router'; +import { ALLOWED_PUBLIC_VERSION, CoreVersionedRouter } from './versioned_router'; import { CoreKibanaRequest } from './request'; import { kibanaResponseFactory } from './response'; import { HapiResponseAdapter } from './response_adapter'; import { wrapErrors } from './error_wrapper'; import { Method } from './versioned_router/types'; -import { prepareRouteConfigValidation } from './util'; +import { getVersionHeader, injectVersionHeader, prepareRouteConfigValidation } from './util'; import { stripIllegalHttp2Headers } from './strip_illegal_http2_headers'; import { validRouteSecurity } from './security_route_config_validator'; import { InternalRouteConfig } from './route'; @@ -171,6 +171,7 @@ export interface InternalRouterRoute extends RouterRoute { /** @internal */ interface InternalGetRoutesOptions { + /** @default false */ excludeVersionedRoutes?: boolean; } @@ -200,10 +201,11 @@ export class Router( route: InternalRouteConfig, handler: RequestHandler, - internalOptions: { isVersioned: boolean } = { isVersioned: false } + { isVersioned }: { isVersioned: boolean } = { isVersioned: false } ) => { route = prepareRouteConfigValidation(route); const routeSchemas = routeSchemasFromRouteConfig(route, method); + const isPublicUnversionedRoute = route.options?.access === 'public' && !isVersioned; this.routes.push({ handler: async (req, responseToolkit) => @@ -211,18 +213,19 @@ export class Router, route.options), /** Below is added for introspection */ validationSchemas: route.validate, - isVersioned: internalOptions.isVersioned, + isVersioned, }); }; @@ -266,10 +269,12 @@ export class Router { it('wraps only expected values in "once"', () => { @@ -49,3 +50,17 @@ describe('prepareResponseValidation', () => { expect(validation.response![500].body).toBeUndefined(); }); }); + +describe('injectResponseHeaders', () => { + it('injects an empty value as expected', () => { + const result = injectResponseHeaders({}, kibanaResponseFactory.ok()); + expect(result.options.headers).toEqual({}); + }); + it('merges values as expected', () => { + const result = injectResponseHeaders( + { foo: 'false', baz: 'true' }, + kibanaResponseFactory.ok({ headers: { foo: 'true', bar: 'false' } }) + ); + expect(result.options.headers).toEqual({ foo: 'false', bar: 'false', baz: 'true' }); + }); +}); diff --git a/packages/core/http/core-http-router-server-internal/src/util.ts b/packages/core/http/core-http-router-server-internal/src/util.ts index 0d1c8abb0e103..176d33b589880 100644 --- a/packages/core/http/core-http-router-server-internal/src/util.ts +++ b/packages/core/http/core-http-router-server-internal/src/util.ts @@ -14,6 +14,9 @@ import { type RouteMethod, type RouteValidator, } from '@kbn/core-http-server'; +import type { Mutable } from 'utility-types'; +import type { IKibanaResponse, ResponseHeaders } from '@kbn/core-http-server'; +import { ELASTIC_HTTP_VERSION_HEADER } from '@kbn/core-http-common'; import type { InternalRouteConfig } from './route'; function isStatusCode(key: string) { @@ -63,3 +66,29 @@ export function prepareRouteConfigValidation( } return config; } + +/** + * @note mutates the response object + * @internal + */ +export function injectResponseHeaders( + headers: ResponseHeaders, + response: IKibanaResponse +): IKibanaResponse { + const mutableResponse = response as Mutable; + mutableResponse.options.headers = { + ...mutableResponse.options.headers, + ...headers, + }; + return mutableResponse; +} + +export function getVersionHeader(version: string): ResponseHeaders { + return { + [ELASTIC_HTTP_VERSION_HEADER]: version, + }; +} + +export function injectVersionHeader(version: string, response: IKibanaResponse): IKibanaResponse { + return injectResponseHeaders(getVersionHeader(version), response); +} diff --git a/packages/core/http/core-http-router-server-internal/src/versioned_router/core_versioned_route.ts b/packages/core/http/core-http-router-server-internal/src/versioned_router/core_versioned_route.ts index 71ab30bbe8b80..e9a9e60de8193 100644 --- a/packages/core/http/core-http-router-server-internal/src/versioned_router/core_versioned_route.ts +++ b/packages/core/http/core-http-router-server-internal/src/versioned_router/core_versioned_route.ts @@ -38,7 +38,7 @@ import { readVersion, removeQueryVersion, } from './route_version_utils'; -import { injectResponseHeaders } from './inject_response_headers'; +import { getVersionHeader, injectVersionHeader } from '../util'; import { validRouteSecurity } from '../security_route_config_validator'; import { resolvers } from './handler_resolvers'; @@ -221,9 +221,7 @@ export class CoreVersionedRoute implements VersionedRoute { req.params = params; req.query = query; } catch (e) { - return res.badRequest({ - body: e.message, - }); + return res.badRequest({ body: e.message, headers: getVersionHeader(version) }); } } else { // Preserve behavior of not passing through unvalidated data @@ -252,12 +250,7 @@ export class CoreVersionedRoute implements VersionedRoute { } } - return injectResponseHeaders( - { - [ELASTIC_HTTP_VERSION_HEADER]: version, - }, - response - ); + return injectVersionHeader(version, response); }; private validateVersion(version: string) { diff --git a/packages/core/http/core-http-router-server-internal/src/versioned_router/inject_response_headers.ts b/packages/core/http/core-http-router-server-internal/src/versioned_router/inject_response_headers.ts deleted file mode 100644 index c27c92023f56e..0000000000000 --- a/packages/core/http/core-http-router-server-internal/src/versioned_router/inject_response_headers.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", the "GNU Affero General Public License v3.0 only", and the "Server Side - * Public License v 1"; you may not use this file except in compliance with, at - * your election, the "Elastic License 2.0", the "GNU Affero General Public - * License v3.0 only", or the "Server Side Public License, v 1". - */ - -import type { Mutable } from 'utility-types'; -import type { IKibanaResponse } from '@kbn/core-http-server'; - -/** - * @note mutates the response object - * @internal - */ -export function injectResponseHeaders(headers: object, response: IKibanaResponse): IKibanaResponse { - const mutableResponse = response as Mutable; - mutableResponse.options = { - ...mutableResponse.options, - headers: { - ...mutableResponse.options.headers, - ...headers, - }, - }; - return mutableResponse; -} diff --git a/packages/core/http/core-http-server-internal/src/http_service.ts b/packages/core/http/core-http-server-internal/src/http_service.ts index e5a82f0abefb0..3f803b06f15fd 100644 --- a/packages/core/http/core-http-server-internal/src/http_service.ts +++ b/packages/core/http/core-http-server-internal/src/http_service.ts @@ -9,9 +9,13 @@ import { Observable, Subscription, combineLatest, firstValueFrom, of, mergeMap } from 'rxjs'; import { map } from 'rxjs'; +import { schema, TypeOf } from '@kbn/config-schema'; import { pick, Semaphore } from '@kbn/std'; -import { generateOpenApiDocument } from '@kbn/router-to-openapispec'; +import { + generateOpenApiDocument, + type GenerateOpenApiDocumentOptionsFilters, +} from '@kbn/router-to-openapispec'; import { Logger } from '@kbn/logging'; import { Env } from '@kbn/config'; import type { CoreContext, CoreService } from '@kbn/core-base-server-internal'; @@ -254,49 +258,55 @@ export class HttpService const baseUrl = basePath.publicBaseUrl ?? `http://localhost:${config.port}${basePath.serverBasePath}`; + const stringOrStringArraySchema = schema.oneOf([ + schema.string(), + schema.arrayOf(schema.string()), + ]); + const querySchema = schema.object({ + access: schema.maybe(schema.oneOf([schema.literal('public'), schema.literal('internal')])), + excludePathsMatching: schema.maybe(stringOrStringArraySchema), + pathStartsWith: schema.maybe(stringOrStringArraySchema), + pluginId: schema.maybe(schema.string()), + version: schema.maybe(schema.string()), + }); + server.route({ path: '/api/oas', method: 'GET', handler: async (req, h) => { - const version = req.query?.version; - - let pathStartsWith: undefined | string[]; - if (typeof req.query?.pathStartsWith === 'string') { - pathStartsWith = [req.query.pathStartsWith]; - } else { - pathStartsWith = req.query?.pathStartsWith; - } - - let excludePathsMatching: undefined | string[]; - if (typeof req.query?.excludePathsMatching === 'string') { - excludePathsMatching = [req.query.excludePathsMatching]; - } else { - excludePathsMatching = req.query?.excludePathsMatching; + let filters: GenerateOpenApiDocumentOptionsFilters; + let query: TypeOf; + try { + query = querySchema.validate(req.query); + filters = { + ...query, + excludePathsMatching: + typeof query.excludePathsMatching === 'string' + ? [query.excludePathsMatching] + : query.excludePathsMatching, + pathStartsWith: + typeof query.pathStartsWith === 'string' + ? [query.pathStartsWith] + : query.pathStartsWith, + }; + } catch (e) { + return h.response({ message: e.message }).code(400); } - - const pluginId = req.query?.pluginId; - - const access = req.query?.access as 'public' | 'internal' | undefined; - if (access && !['public', 'internal'].some((a) => a === access)) { - return h - .response({ - message: 'Invalid access query parameter. Must be one of "public" or "internal".', - }) - .code(400); - } - return await firstValueFrom( of(1).pipe( HttpService.generateOasSemaphore.acquire(), mergeMap(async () => { try { // Potentially quite expensive - const result = generateOpenApiDocument(this.httpServer.getRouters({ pluginId }), { - baseUrl, - title: 'Kibana HTTP APIs', - version: '0.0.0', // TODO get a better version here - filters: { pathStartsWith, excludePathsMatching, access, version }, - }); + const result = generateOpenApiDocument( + this.httpServer.getRouters({ pluginId: query.pluginId }), + { + baseUrl, + title: 'Kibana HTTP APIs', + version: '0.0.0', // TODO get a better version here + filters, + } + ); return h.response(result); } catch (e) { this.log.error(e); diff --git a/packages/core/http/core-http-server/src/router/route.ts b/packages/core/http/core-http-server/src/router/route.ts index bdf4f9f03c784..194191e6f423f 100644 --- a/packages/core/http/core-http-server/src/router/route.ts +++ b/packages/core/http/core-http-server/src/router/route.ts @@ -215,7 +215,7 @@ export interface RouteConfigOptions { /** * Defines intended request origin of the route: * - public. The route is public, declared stable and intended for external access. - * In the future, may require an incomming request to contain a specified header. + * In the future, may require an incoming request to contain a specified header. * - internal. The route is internal and intended for internal access only. * * Defaults to 'internal' If not declared, @@ -284,6 +284,14 @@ export interface RouteConfigOptions { */ deprecated?: boolean; + /** + * Whether this route should be treated as "invisible" and excluded from router + * OAS introspection. + * + * @default false + */ + excludeFromOAS?: boolean; + /** * Release version or date that this route will be removed * Use with `deprecated: true` @@ -292,6 +300,7 @@ export interface RouteConfigOptions { * @example 9.0.0 */ discontinued?: string; + /** * Defines the security requirements for a route, including authorization and authentication. * diff --git a/packages/core/plugins/core-plugins-server-internal/src/plugin.ts b/packages/core/plugins/core-plugins-server-internal/src/plugin.ts index 8837cb24083d6..cd330a647da66 100644 --- a/packages/core/plugins/core-plugins-server-internal/src/plugin.ts +++ b/packages/core/plugins/core-plugins-server-internal/src/plugin.ts @@ -15,7 +15,6 @@ import { isConfigSchema } from '@kbn/config-schema'; import type { Logger } from '@kbn/logging'; import { type PluginOpaqueId, PluginType } from '@kbn/core-base-common'; import type { - AsyncPlugin, Plugin, PluginConfigDescriptor, PluginInitializer, @@ -58,8 +57,7 @@ export class PluginWrapper< private instance?: | Plugin - | PrebootPlugin - | AsyncPlugin; + | PrebootPlugin; private readonly startDependencies$ = new Subject<[CoreStart, TPluginsStart, TStart]>(); public readonly startDependencies = firstValueFrom(this.startDependencies$); diff --git a/packages/core/plugins/core-plugins-server/index.ts b/packages/core/plugins/core-plugins-server/index.ts index b2c6057c4a1ac..a5fd0fd2e2ec3 100644 --- a/packages/core/plugins/core-plugins-server/index.ts +++ b/packages/core/plugins/core-plugins-server/index.ts @@ -10,7 +10,6 @@ export type { PrebootPlugin, Plugin, - AsyncPlugin, PluginConfigDescriptor, PluginConfigSchema, PluginInitializer, diff --git a/packages/core/plugins/core-plugins-server/src/index.ts b/packages/core/plugins/core-plugins-server/src/index.ts index 35b1b7c11d422..e48d077389ece 100644 --- a/packages/core/plugins/core-plugins-server/src/index.ts +++ b/packages/core/plugins/core-plugins-server/src/index.ts @@ -10,7 +10,6 @@ export type { PrebootPlugin, Plugin, - AsyncPlugin, PluginConfigDescriptor, PluginConfigSchema, PluginInitializer, diff --git a/packages/core/plugins/core-plugins-server/src/types.ts b/packages/core/plugins/core-plugins-server/src/types.ts index 6da8b2727733e..7be2647ba48d2 100644 --- a/packages/core/plugins/core-plugins-server/src/types.ts +++ b/packages/core/plugins/core-plugins-server/src/types.ts @@ -301,26 +301,6 @@ export interface Plugin< stop?(): MaybePromise; } -/** - * A plugin with asynchronous lifecycle methods. - * - * @deprecated Asynchronous lifecycles are deprecated, and should be migrated to sync {@link Plugin | plugin} - * @removeBy 8.8.0 - * @public - */ -export interface AsyncPlugin< - TSetup = void, - TStart = void, - TPluginsSetup extends object = object, - TPluginsStart extends object = object -> { - setup(core: CoreSetup, plugins: TPluginsSetup): TSetup | Promise; - - start(core: CoreStart, plugins: TPluginsStart): TStart | Promise; - - stop?(): MaybePromise; -} - /** * @public */ @@ -478,7 +458,5 @@ export type PluginInitializer< > = ( core: PluginInitializerContext ) => Promise< - | Plugin - | PrebootPlugin - | AsyncPlugin + Plugin | PrebootPlugin >; diff --git a/packages/kbn-ace/README.md b/packages/kbn-ace/README.md deleted file mode 100644 index c11d5cc2f24b8..0000000000000 --- a/packages/kbn-ace/README.md +++ /dev/null @@ -1,20 +0,0 @@ -# @kbn/ace - -This package contains the XJSON mode for brace. This is an extension of the `brace/mode/json` mode. - -This package also contains an import of the entire brace editor which is used for creating the custom XJSON worker. - -## Note to plugins -_This code should not be eagerly loaded_. - -Make sure imports of this package are behind a lazy-load `import()` statement. - -Your plugin should already be loading application code this way in the `mount` function. - -## Deprecated - -This package is considered deprecated and will be removed in future. - -New and existing editor functionality should use Monaco. - -_Do not add new functionality to this package_. Build new functionality for Monaco and use it instead. diff --git a/packages/kbn-ace/index.ts b/packages/kbn-ace/index.ts deleted file mode 100644 index c9cc0b7a73e86..0000000000000 --- a/packages/kbn-ace/index.ts +++ /dev/null @@ -1,17 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the "Elastic License - * 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side - * Public License v 1"; you may not use this file except in compliance with, at - * your election, the "Elastic License 2.0", the "GNU Affero General Public - * License v3.0 only", or the "Server Side Public License, v 1". - */ - -export { - ElasticsearchSqlHighlightRules, - ScriptHighlightRules, - XJsonHighlightRules, - addXJsonToRules, - XJsonMode, - installXJsonMode, -} from './src/ace/modes'; diff --git a/packages/kbn-ace/kibana.jsonc b/packages/kbn-ace/kibana.jsonc deleted file mode 100644 index 0a01d96a6b1c6..0000000000000 --- a/packages/kbn-ace/kibana.jsonc +++ /dev/null @@ -1,5 +0,0 @@ -{ - "type": "shared-common", - "id": "@kbn/ace", - "owner": "@elastic/kibana-management" -} diff --git a/packages/kbn-ace/package.json b/packages/kbn-ace/package.json deleted file mode 100644 index 3d3ed36941978..0000000000000 --- a/packages/kbn-ace/package.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "name": "@kbn/ace", - "version": "1.0.0", - "private": true, - "license": "Elastic License 2.0 OR AGPL-3.0-only OR SSPL-1.0" -} \ No newline at end of file diff --git a/packages/kbn-ace/src/ace/modes/index.ts b/packages/kbn-ace/src/ace/modes/index.ts deleted file mode 100644 index ffbb385663e48..0000000000000 --- a/packages/kbn-ace/src/ace/modes/index.ts +++ /dev/null @@ -1,17 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the "Elastic License - * 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side - * Public License v 1"; you may not use this file except in compliance with, at - * your election, the "Elastic License 2.0", the "GNU Affero General Public - * License v3.0 only", or the "Server Side Public License, v 1". - */ - -export { - ElasticsearchSqlHighlightRules, - ScriptHighlightRules, - XJsonHighlightRules, - addXJsonToRules, -} from './lexer_rules'; - -export { installXJsonMode, XJsonMode } from './x_json'; diff --git a/packages/kbn-ace/src/ace/modes/lexer_rules/elasticsearch_sql_highlight_rules.ts b/packages/kbn-ace/src/ace/modes/lexer_rules/elasticsearch_sql_highlight_rules.ts deleted file mode 100644 index a4cb60529281d..0000000000000 --- a/packages/kbn-ace/src/ace/modes/lexer_rules/elasticsearch_sql_highlight_rules.ts +++ /dev/null @@ -1,104 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the "Elastic License - * 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side - * Public License v 1"; you may not use this file except in compliance with, at - * your election, the "Elastic License 2.0", the "GNU Affero General Public - * License v3.0 only", or the "Server Side Public License, v 1". - */ - -import ace from 'brace'; - -const { TextHighlightRules } = ace.acequire('ace/mode/text_highlight_rules'); -const oop = ace.acequire('ace/lib/oop'); - -export const ElasticsearchSqlHighlightRules = function (this: any) { - // See https://www.elastic.co/guide/en/elasticsearch/reference/current/sql-commands.html - const keywords = - 'describe|between|in|like|not|and|or|desc|select|from|where|having|group|by|order' + - 'asc|desc|pivot|for|in|as|show|columns|include|frozen|tables|escape|limit|rlike|all|distinct|is'; - - const builtinConstants = 'true|false'; - - // See https://www.elastic.co/guide/en/elasticsearch/reference/current/sql-syntax-show-functions.html - const builtinFunctions = - 'avg|count|first|first_value|last|last_value|max|min|sum|kurtosis|mad|percentile|percentile_rank|skewness' + - '|stddev_pop|sum_of_squares|var_pop|histogram|case|coalesce|greatest|ifnull|iif|isnull|least|nullif|nvl' + - '|curdate|current_date|current_time|current_timestamp|curtime|dateadd|datediff|datepart|datetrunc|date_add' + - '|date_diff|date_part|date_trunc|day|dayname|dayofmonth|dayofweek|dayofyear|day_name|day_of_month|day_of_week' + - '|day_of_year|dom|dow|doy|hour|hour_of_day|idow|isodayofweek|isodow|isoweek|isoweekofyear|iso_day_of_week|iso_week_of_year' + - '|iw|iwoy|minute|minute_of_day|minute_of_hour|month|monthname|month_name|month_of_year|now|quarter|second|second_of_minute' + - '|timestampadd|timestampdiff|timestamp_add|timestamp_diff|today|week|week_of_year|year|abs|acos|asin|atan|atan2|cbrt' + - '|ceil|ceiling|cos|cosh|cot|degrees|e|exp|expm1|floor|log|log10|mod|pi|power|radians|rand|random|round|sign|signum|sin' + - '|sinh|sqrt|tan|truncate|ascii|bit_length|char|character_length|char_length|concat|insert|lcase|left|length|locate' + - '|ltrim|octet_length|position|repeat|replace|right|rtrim|space|substring|ucase|cast|convert|database|user|st_astext|st_aswkt' + - '|st_distance|st_geometrytype|st_geomfromtext|st_wkttosql|st_x|st_y|st_z|score'; - - // See https://www.elastic.co/guide/en/elasticsearch/reference/current/sql-data-types.html - const dataTypes = - 'null|boolean|byte|short|integer|long|double|float|half_float|scaled_float|keyword|text|binary|date|ip|object|nested|time' + - '|interval_year|interval_month|interval_day|interval_hour|interval_minute|interval_second|interval_year_to_month' + - 'inteval_day_to_hour|interval_day_to_minute|interval_day_to_second|interval_hour_to_minute|interval_hour_to_second' + - 'interval_minute_to_second|geo_point|geo_shape|shape'; - - const keywordMapper = this.createKeywordMapper( - { - keyword: [keywords, builtinFunctions, builtinConstants, dataTypes].join('|'), - }, - 'identifier', - true - ); - - this.$rules = { - start: [ - { - token: 'comment', - regex: '--.*$', - }, - { - token: 'comment', - start: '/\\*', - end: '\\*/', - }, - { - token: 'string', // " string - regex: '".*?"', - }, - { - token: 'constant', // ' string - regex: "'.*?'", - }, - { - token: 'string', // ` string (apache drill) - regex: '`.*?`', - }, - { - token: 'entity.name.function', // float - regex: '[+-]?\\d+(?:(?:\\.\\d*)?(?:[eE][+-]?\\d+)?)?\\b', - }, - { - token: keywordMapper, - regex: '[a-zA-Z_$][a-zA-Z0-9_$]*\\b', - }, - { - token: 'keyword.operator', - regex: '⇐|<⇒|\\*|\\.|\\:\\:|\\+|\\-|\\/|\\/\\/|%|&|\\^|~|<|>|<=|=>|==|!=|<>|=', - }, - { - token: 'paren.lparen', - regex: '[\\(]', - }, - { - token: 'paren.rparen', - regex: '[\\)]', - }, - { - token: 'text', - regex: '\\s+', - }, - ], - }; - this.normalizeRules(); -}; - -oop.inherits(ElasticsearchSqlHighlightRules, TextHighlightRules); diff --git a/packages/kbn-ace/src/ace/modes/lexer_rules/index.ts b/packages/kbn-ace/src/ace/modes/lexer_rules/index.ts deleted file mode 100644 index aa8c6af19c10f..0000000000000 --- a/packages/kbn-ace/src/ace/modes/lexer_rules/index.ts +++ /dev/null @@ -1,12 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the "Elastic License - * 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side - * Public License v 1"; you may not use this file except in compliance with, at - * your election, the "Elastic License 2.0", the "GNU Affero General Public - * License v3.0 only", or the "Server Side Public License, v 1". - */ - -export { ElasticsearchSqlHighlightRules } from './elasticsearch_sql_highlight_rules'; -export { ScriptHighlightRules } from './script_highlight_rules'; -export { XJsonHighlightRules, addToRules as addXJsonToRules } from './x_json_highlight_rules'; diff --git a/packages/kbn-ace/src/ace/modes/lexer_rules/script_highlight_rules.ts b/packages/kbn-ace/src/ace/modes/lexer_rules/script_highlight_rules.ts deleted file mode 100644 index 64e8a1a6594bd..0000000000000 --- a/packages/kbn-ace/src/ace/modes/lexer_rules/script_highlight_rules.ts +++ /dev/null @@ -1,73 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the "Elastic License - * 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side - * Public License v 1"; you may not use this file except in compliance with, at - * your election, the "Elastic License 2.0", the "GNU Affero General Public - * License v3.0 only", or the "Server Side Public License, v 1". - */ - -import ace from 'brace'; -const oop = ace.acequire('ace/lib/oop'); -const { TextHighlightRules } = ace.acequire('ace/mode/text_highlight_rules'); -const painlessKeywords = - 'def|int|long|byte|String|float|double|char|null|if|else|while|do|for|continue|break|new|try|catch|throw|this|instanceof|return|ctx'; - -export function ScriptHighlightRules(this: any) { - this.name = 'ScriptHighlightRules'; - this.$rules = { - start: [ - { - token: 'script.comment', - regex: '\\/\\/.*$', - }, - { - token: 'script.string.regexp', - regex: '[/](?:(?:\\[(?:\\\\]|[^\\]])+\\])|(?:\\\\/|[^\\]/]))*[/]\\w*\\s*(?=[).,;]|$)', - }, - { - token: 'script.string', // single line - regex: "['](?:(?:\\\\.)|(?:[^'\\\\]))*?[']", - }, - { - token: 'script.constant.numeric', // hex - regex: '0[xX][0-9a-fA-F]+\\b', - }, - { - token: 'script.constant.numeric', // float - regex: '[+-]?\\d+(?:(?:\\.\\d*)?(?:[eE][+-]?\\d+)?)?\\b', - }, - { - token: 'script.constant.language.boolean', - regex: '(?:true|false)\\b', - }, - { - token: 'script.keyword', - regex: painlessKeywords, - }, - { - token: 'script.text', - regex: '[a-zA-Z_$][a-zA-Z0-9_$]*\\b', - }, - { - token: 'script.keyword.operator', - regex: - '\\?\\.|\\*\\.|=~|==~|!|%|&|\\*|\\-\\-|\\-|\\+\\+|\\+|~|===|==|=|!=|!==|<=|>=|<<=|>>=|>>>=|<>|<|>|->|!|&&|\\|\\||\\?\\:|\\*=|%=|\\+=|\\-=|&=|\\^=|\\b(?:in|instanceof|new|typeof|void)', - }, - { - token: 'script.lparen', - regex: '[[({]', - }, - { - token: 'script.rparen', - regex: '[\\])}]', - }, - { - token: 'script.text', - regex: '\\s+', - }, - ], - }; -} - -oop.inherits(ScriptHighlightRules, TextHighlightRules); diff --git a/packages/kbn-ace/src/ace/modes/lexer_rules/x_json_highlight_rules.ts b/packages/kbn-ace/src/ace/modes/lexer_rules/x_json_highlight_rules.ts deleted file mode 100644 index f69e2fbbf5d8a..0000000000000 --- a/packages/kbn-ace/src/ace/modes/lexer_rules/x_json_highlight_rules.ts +++ /dev/null @@ -1,184 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the "Elastic License - * 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side - * Public License v 1"; you may not use this file except in compliance with, at - * your election, the "Elastic License 2.0", the "GNU Affero General Public - * License v3.0 only", or the "Server Side Public License, v 1". - */ - -import { defaultsDeep } from 'lodash'; -import ace from 'brace'; -import 'brace/mode/json'; - -import { ElasticsearchSqlHighlightRules } from './elasticsearch_sql_highlight_rules'; -import { ScriptHighlightRules } from './script_highlight_rules'; - -const { JsonHighlightRules } = ace.acequire('ace/mode/json_highlight_rules'); -const oop = ace.acequire('ace/lib/oop'); - -const jsonRules = function (root: any) { - root = root ? root : 'json'; - const rules: any = {}; - const xJsonRules = [ - { - token: [ - 'variable', - 'whitespace', - 'ace.punctuation.colon', - 'whitespace', - 'punctuation.start_triple_quote', - ], - regex: '("(?:[^"]*_)?script"|"inline"|"source")(\\s*?)(:)(\\s*?)(""")', - next: 'script-start', - merge: false, - push: true, - }, - { - token: 'variable', // single line - regex: '["](?:(?:\\\\.)|(?:[^"\\\\]))*?["]\\s*(?=:)', - }, - { - token: 'punctuation.start_triple_quote', - regex: '"""', - next: 'string_literal', - merge: false, - push: true, - }, - { - token: 'string', // single line - regex: '["](?:(?:\\\\.)|(?:[^"\\\\]))*?["]', - }, - { - token: 'constant.numeric', // hex - regex: '0[xX][0-9a-fA-F]+\\b', - }, - { - token: 'constant.numeric', // float - regex: '[+-]?\\d+(?:(?:\\.\\d*)?(?:[eE][+-]?\\d+)?)?\\b', - }, - { - token: 'constant.language.boolean', - regex: '(?:true|false)\\b', - }, - { - token: 'invalid.illegal', // single quoted strings are not allowed - regex: "['](?:(?:\\\\.)|(?:[^'\\\\]))*?[']", - }, - { - token: 'invalid.illegal', // comments are not allowed - regex: '\\/\\/.*$', - }, - { - token: 'paren.lparen', - merge: false, - regex: '{', - next: root, - push: true, - }, - { - token: 'paren.lparen', - merge: false, - regex: '[[(]', - }, - { - token: 'paren.rparen', - merge: false, - regex: '[\\])]', - }, - { - token: 'paren.rparen', - regex: '}', - merge: false, - next: 'pop', - }, - { - token: 'punctuation.comma', - regex: ',', - }, - { - token: 'punctuation.colon', - regex: ':', - }, - { - token: 'whitespace', - regex: '\\s+', - }, - { - token: 'text', - regex: '.+?', - }, - ]; - - rules[root] = xJsonRules; - rules[root + '-sql'] = [ - { - token: [ - 'variable', - 'whitespace', - 'ace.punctuation.colon', - 'whitespace', - 'punctuation.start_triple_quote', - ], - regex: '("query")(\\s*?)(:)(\\s*?)(""")', - next: 'sql-start', - merge: false, - push: true, - }, - ].concat(xJsonRules as any); - - rules.string_literal = [ - { - token: 'punctuation.end_triple_quote', - regex: '"""', - next: 'pop', - }, - { - token: 'multi_string', - regex: '.', - }, - ]; - return rules; -}; - -export function XJsonHighlightRules(this: any) { - this.$rules = { - ...jsonRules('start'), - }; - - this.embedRules(ScriptHighlightRules, 'script-', [ - { - token: 'punctuation.end_triple_quote', - regex: '"""', - next: 'pop', - }, - ]); - - this.embedRules(ElasticsearchSqlHighlightRules, 'sql-', [ - { - token: 'punctuation.end_triple_quote', - regex: '"""', - next: 'pop', - }, - ]); -} - -oop.inherits(XJsonHighlightRules, JsonHighlightRules); - -export function addToRules(otherRules: any, embedUnder: any) { - otherRules.$rules = defaultsDeep(otherRules.$rules, jsonRules(embedUnder)); - otherRules.embedRules(ScriptHighlightRules, 'script-', [ - { - token: 'punctuation.end_triple_quote', - regex: '"""', - next: 'pop', - }, - ]); - otherRules.embedRules(ElasticsearchSqlHighlightRules, 'sql-', [ - { - token: 'punctuation.end_triple_quote', - regex: '"""', - next: 'pop', - }, - ]); -} diff --git a/packages/kbn-ace/src/ace/modes/x_json/index.ts b/packages/kbn-ace/src/ace/modes/x_json/index.ts deleted file mode 100644 index a1651c9e06979..0000000000000 --- a/packages/kbn-ace/src/ace/modes/x_json/index.ts +++ /dev/null @@ -1,10 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the "Elastic License - * 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side - * Public License v 1"; you may not use this file except in compliance with, at - * your election, the "Elastic License 2.0", the "GNU Affero General Public - * License v3.0 only", or the "Server Side Public License, v 1". - */ - -export { installXJsonMode, XJsonMode } from './x_json'; diff --git a/packages/kbn-ace/src/ace/modes/x_json/worker/worker.d.ts b/packages/kbn-ace/src/ace/modes/x_json/worker/worker.d.ts deleted file mode 100644 index 34598ea61003b..0000000000000 --- a/packages/kbn-ace/src/ace/modes/x_json/worker/worker.d.ts +++ /dev/null @@ -1,15 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the "Elastic License - * 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side - * Public License v 1"; you may not use this file except in compliance with, at - * your election, the "Elastic License 2.0", the "GNU Affero General Public - * License v3.0 only", or the "Server Side Public License, v 1". - */ - -// Satisfy TS's requirements that the module be declared per './index.ts'. -declare module '!!raw-loader!./worker.js' { - const content: string; - // eslint-disable-next-line import/no-default-export - export default content; -} diff --git a/packages/kbn-ace/src/ace/modes/x_json/worker/x_json.ace.worker.js b/packages/kbn-ace/src/ace/modes/x_json/worker/x_json.ace.worker.js deleted file mode 100644 index 63ca258e524d4..0000000000000 --- a/packages/kbn-ace/src/ace/modes/x_json/worker/x_json.ace.worker.js +++ /dev/null @@ -1,1265 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the "Elastic License - * 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side - * Public License v 1"; you may not use this file except in compliance with, at - * your election, the "Elastic License 2.0", the "GNU Affero General Public - * License v3.0 only", or the "Server Side Public License, v 1". - */ - -/* @notice - * - * This product includes code that is based on Ace editor, which was available - * under a "BSD" license. - * - * Distributed under the BSD license: - * - * Copyright (c) 2010, Ajax.org B.V. - * All rights reserved. - * - * Redistribution and use in source and binary forms, with or without - * modification, are permitted provided that the following conditions are met: - * * Redistributions of source code must retain the above copyright - * notice, this list of conditions and the following disclaimer. - * * Redistributions in binary form must reproduce the above copyright - * notice, this list of conditions and the following disclaimer in the - * documentation and/or other materials provided with the distribution. - * * Neither the name of Ajax.org B.V. nor the - * names of its contributors may be used to endorse or promote products - * derived from this software without specific prior written permission. - * - * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND - * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED - * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE - * DISCLAIMED. IN NO EVENT SHALL AJAX.ORG B.V. BE LIABLE FOR ANY - * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES - * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; - * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND - * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT - * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS - * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - */ - -/* eslint-disable prettier/prettier,no-var,eqeqeq,no-use-before-define,block-scoped-var,no-undef, - guard-for-in,one-var,strict,no-redeclare,no-sequences,no-proto,new-cap,no-nested-ternary,no-unused-vars, - prefer-const,no-empty,no-extend-native,camelcase */ -/* - This file is loaded up as a blob by Brace to hand to Ace to load as Jsonp - (hence the redefining of everything). It is based on the json - mode from the brace distro. - - It is very likely that this file will be removed in future but for now it enables - extended JSON parsing, like e.g. """{}""" (triple quotes) -*/ -// @internal -// @ts-nocheck -"no use strict"; -! function(window) { - function resolveModuleId(id, paths) { - for (var testPath = id, tail = ""; testPath;) { - var alias = paths[testPath]; - if ("string" == typeof alias) return alias + tail; - if (alias) return alias.location.replace(/\/*$/, "/") + (tail || alias.main || alias.name); - if (alias === !1) return ""; - var i = testPath.lastIndexOf("/"); - if (-1 === i) break; - tail = testPath.substr(i) + tail, testPath = testPath.slice(0, i) - } - return id - } - if (!(void 0 !== window.window && window.document || window.acequire && window.define)) { - window.console || (window.console = function() { - var msgs = Array.prototype.slice.call(arguments, 0); - postMessage({ - type: "log", - data: msgs - }) - }, window.console.error = window.console.warn = window.console.log = window.console.trace = window.console), window.window = window, window.ace = window, window.onerror = function(message, file, line, col, err) { - postMessage({ - type: "error", - data: { - message: message, - data: err.data, - file: file, - line: line, - col: col, - stack: err.stack - } - }) - }, window.normalizeModule = function(parentId, moduleName) { - if (-1 !== moduleName.indexOf("!")) { - var chunks = moduleName.split("!"); - return window.normalizeModule(parentId, chunks[0]) + "!" + window.normalizeModule(parentId, chunks[1]) - } - if ("." == moduleName.charAt(0)) { - var base = parentId.split("/").slice(0, -1).join("/"); - for (moduleName = (base ? base + "/" : "") + moduleName; - 1 !== moduleName.indexOf(".") && previous != moduleName;) { - var previous = moduleName; - moduleName = moduleName.replace(/^\.\//, "").replace(/\/\.\//, "/").replace(/[^\/]+\/\.\.\//, "") - } - } - return moduleName - }, window.acequire = function acequire(parentId, id) { - if (id || (id = parentId, parentId = null), !id.charAt) throw Error("worker.js acequire() accepts only (parentId, id) as arguments"); - id = window.normalizeModule(parentId, id); - var module = window.acequire.modules[id]; - if (module) return module.initialized || (module.initialized = !0, module.exports = module.factory().exports), module.exports; - if (!window.acequire.tlns) return console.log("unable to load " + id); - var path = resolveModuleId(id, window.acequire.tlns); - return ".js" != path.slice(-3) && (path += ".js"), window.acequire.id = id, window.acequire.modules[id] = {}, importScripts(path), window.acequire(parentId, id) - }, window.acequire.modules = {}, window.acequire.tlns = {}, window.define = function(id, deps, factory) { - if (2 == arguments.length ? (factory = deps, "string" != typeof id && (deps = id, id = window.acequire.id)) : 1 == arguments.length && (factory = id, deps = [], id = window.acequire.id), "function" != typeof factory) return window.acequire.modules[id] = { - exports: factory, - initialized: !0 - }, void 0; - deps.length || (deps = ["require", "exports", "module"]); - var req = function(childId) { - return window.acequire(id, childId) - }; - window.acequire.modules[id] = { - exports: {}, - factory: function() { - var module = this, - returnExports = factory.apply(this, deps.map(function(dep) { - switch (dep) { - case "require": - return req; - case "exports": - return module.exports; - case "module": - return module; - default: - return req(dep) - } - })); - return returnExports && (module.exports = returnExports), module - } - } - }, window.define.amd = {}, acequire.tlns = {}, window.initBaseUrls = function(topLevelNamespaces) { - for (var i in topLevelNamespaces) acequire.tlns[i] = topLevelNamespaces[i] - }, window.initSender = function() { - var EventEmitter = window.acequire("ace/lib/event_emitter").EventEmitter, - oop = window.acequire("ace/lib/oop"), - Sender = function() {}; - return function() { - oop.implement(this, EventEmitter), this.callback = function(data, callbackId) { - postMessage({ - type: "call", - id: callbackId, - data: data - }) - }, this.emit = function(name, data) { - postMessage({ - type: "event", - name: name, - data: data - }) - } - }.call(Sender.prototype), new Sender - }; - var main = window.main = null, - sender = window.sender = null; - window.onmessage = function(e) { - var msg = e.data; - if (msg.event && sender) sender._signal(msg.event, msg.data); - else if (msg.command) - if (main[msg.command]) main[msg.command].apply(main, msg.args); - else { - if (!window[msg.command]) throw Error("Unknown command:" + msg.command); - window[msg.command].apply(window, msg.args) - } - else if (msg.init) { - window.initBaseUrls(msg.tlns), acequire("ace/lib/es5-shim"), sender = window.sender = window.initSender(); - var clazz = acequire(msg.module)[msg.classname]; - main = window.main = new clazz(sender) - } - } - } -}(this), ace.define("ace/lib/oop", ["require", "exports", "module"], function(acequire, exports) { - "use strict"; - exports.inherits = function(ctor, superCtor) { - ctor.super_ = superCtor, ctor.prototype = Object.create(superCtor.prototype, { - constructor: { - value: ctor, - enumerable: !1, - writable: !0, - configurable: !0 - } - }) - }, exports.mixin = function(obj, mixin) { - for (var key in mixin) obj[key] = mixin[key]; - return obj - }, exports.implement = function(proto, mixin) { - exports.mixin(proto, mixin) - } -}), ace.define("ace/range", ["require", "exports", "module"], function(acequire, exports) { - "use strict"; - var comparePoints = function(p1, p2) { - return p1.row - p2.row || p1.column - p2.column - }, - Range = function(startRow, startColumn, endRow, endColumn) { - this.start = { - row: startRow, - column: startColumn - }, this.end = { - row: endRow, - column: endColumn - } - }; - (function() { - this.isEqual = function(range) { - return this.start.row === range.start.row && this.end.row === range.end.row && this.start.column === range.start.column && this.end.column === range.end.column - }, this.toString = function() { - return "Range: [" + this.start.row + "/" + this.start.column + "] -> [" + this.end.row + "/" + this.end.column + "]" - }, this.contains = function(row, column) { - return 0 == this.compare(row, column) - }, this.compareRange = function(range) { - var cmp, end = range.end, - start = range.start; - return cmp = this.compare(end.row, end.column), 1 == cmp ? (cmp = this.compare(start.row, start.column), 1 == cmp ? 2 : 0 == cmp ? 1 : 0) : -1 == cmp ? -2 : (cmp = this.compare(start.row, start.column), -1 == cmp ? -1 : 1 == cmp ? 42 : 0) - }, this.comparePoint = function(p) { - return this.compare(p.row, p.column) - }, this.containsRange = function(range) { - return 0 == this.comparePoint(range.start) && 0 == this.comparePoint(range.end) - }, this.intersects = function(range) { - var cmp = this.compareRange(range); - return -1 == cmp || 0 == cmp || 1 == cmp - }, this.isEnd = function(row, column) { - return this.end.row == row && this.end.column == column - }, this.isStart = function(row, column) { - return this.start.row == row && this.start.column == column - }, this.setStart = function(row, column) { - "object" == typeof row ? (this.start.column = row.column, this.start.row = row.row) : (this.start.row = row, this.start.column = column) - }, this.setEnd = function(row, column) { - "object" == typeof row ? (this.end.column = row.column, this.end.row = row.row) : (this.end.row = row, this.end.column = column) - }, this.inside = function(row, column) { - return 0 == this.compare(row, column) ? this.isEnd(row, column) || this.isStart(row, column) ? !1 : !0 : !1 - }, this.insideStart = function(row, column) { - return 0 == this.compare(row, column) ? this.isEnd(row, column) ? !1 : !0 : !1 - }, this.insideEnd = function(row, column) { - return 0 == this.compare(row, column) ? this.isStart(row, column) ? !1 : !0 : !1 - }, this.compare = function(row, column) { - return this.isMultiLine() || row !== this.start.row ? this.start.row > row ? -1 : row > this.end.row ? 1 : this.start.row === row ? column >= this.start.column ? 0 : -1 : this.end.row === row ? this.end.column >= column ? 0 : 1 : 0 : this.start.column > column ? -1 : column > this.end.column ? 1 : 0 - }, this.compareStart = function(row, column) { - return this.start.row == row && this.start.column == column ? -1 : this.compare(row, column) - }, this.compareEnd = function(row, column) { - return this.end.row == row && this.end.column == column ? 1 : this.compare(row, column) - }, this.compareInside = function(row, column) { - return this.end.row == row && this.end.column == column ? 1 : this.start.row == row && this.start.column == column ? -1 : this.compare(row, column) - }, this.clipRows = function(firstRow, lastRow) { - if (this.end.row > lastRow) var end = { - row: lastRow + 1, - column: 0 - }; - else if (firstRow > this.end.row) var end = { - row: firstRow, - column: 0 - }; - if (this.start.row > lastRow) var start = { - row: lastRow + 1, - column: 0 - }; - else if (firstRow > this.start.row) var start = { - row: firstRow, - column: 0 - }; - return Range.fromPoints(start || this.start, end || this.end) - }, this.extend = function(row, column) { - var cmp = this.compare(row, column); - if (0 == cmp) return this; - if (-1 == cmp) var start = { - row: row, - column: column - }; - else var end = { - row: row, - column: column - }; - return Range.fromPoints(start || this.start, end || this.end) - }, this.isEmpty = function() { - return this.start.row === this.end.row && this.start.column === this.end.column - }, this.isMultiLine = function() { - return this.start.row !== this.end.row - }, this.clone = function() { - return Range.fromPoints(this.start, this.end) - }, this.collapseRows = function() { - return 0 == this.end.column ? new Range(this.start.row, 0, Math.max(this.start.row, this.end.row - 1), 0) : new Range(this.start.row, 0, this.end.row, 0) - }, this.toScreenRange = function(session) { - var screenPosStart = session.documentToScreenPosition(this.start), - screenPosEnd = session.documentToScreenPosition(this.end); - return new Range(screenPosStart.row, screenPosStart.column, screenPosEnd.row, screenPosEnd.column) - }, this.moveBy = function(row, column) { - this.start.row += row, this.start.column += column, this.end.row += row, this.end.column += column - } - }).call(Range.prototype), Range.fromPoints = function(start, end) { - return new Range(start.row, start.column, end.row, end.column) - }, Range.comparePoints = comparePoints, Range.comparePoints = function(p1, p2) { - return p1.row - p2.row || p1.column - p2.column - }, exports.Range = Range -}), ace.define("ace/apply_delta", ["require", "exports", "module"], function(acequire, exports) { - "use strict"; - exports.applyDelta = function(docLines, delta) { - var row = delta.start.row, - startColumn = delta.start.column, - line = docLines[row] || ""; - switch (delta.action) { - case "insert": - var lines = delta.lines; - if (1 === lines.length) docLines[row] = line.substring(0, startColumn) + delta.lines[0] + line.substring(startColumn); - else { - var args = [row, 1].concat(delta.lines); - docLines.splice.apply(docLines, args), docLines[row] = line.substring(0, startColumn) + docLines[row], docLines[row + delta.lines.length - 1] += line.substring(startColumn) - } - break; - case "remove": - var endColumn = delta.end.column, - endRow = delta.end.row; - row === endRow ? docLines[row] = line.substring(0, startColumn) + line.substring(endColumn) : docLines.splice(row, endRow - row + 1, line.substring(0, startColumn) + docLines[endRow].substring(endColumn)) - } - } -}), ace.define("ace/lib/event_emitter", ["require", "exports", "module"], function(acequire, exports) { - "use strict"; - var EventEmitter = {}, - stopPropagation = function() { - this.propagationStopped = !0 - }, - preventDefault = function() { - this.defaultPrevented = !0 - }; - EventEmitter._emit = EventEmitter._dispatchEvent = function(eventName, e) { - this._eventRegistry || (this._eventRegistry = {}), this._defaultHandlers || (this._defaultHandlers = {}); - var listeners = this._eventRegistry[eventName] || [], - defaultHandler = this._defaultHandlers[eventName]; - if (listeners.length || defaultHandler) { - "object" == typeof e && e || (e = {}), e.type || (e.type = eventName), e.stopPropagation || (e.stopPropagation = stopPropagation), e.preventDefault || (e.preventDefault = preventDefault), listeners = listeners.slice(); - for (var i = 0; listeners.length > i && (listeners[i](e, this), !e.propagationStopped); i++); - return defaultHandler && !e.defaultPrevented ? defaultHandler(e, this) : void 0 - } - }, EventEmitter._signal = function(eventName, e) { - var listeners = (this._eventRegistry || {})[eventName]; - if (listeners) { - listeners = listeners.slice(); - for (var i = 0; listeners.length > i; i++) listeners[i](e, this) - } - }, EventEmitter.once = function(eventName, callback) { - var _self = this; - callback && this.addEventListener(eventName, function newCallback() { - _self.removeEventListener(eventName, newCallback), callback.apply(null, arguments) - }) - }, EventEmitter.setDefaultHandler = function(eventName, callback) { - var handlers = this._defaultHandlers; - if (handlers || (handlers = this._defaultHandlers = { - _disabled_: {} - }), handlers[eventName]) { - var old = handlers[eventName], - disabled = handlers._disabled_[eventName]; - disabled || (handlers._disabled_[eventName] = disabled = []), disabled.push(old); - var i = disabled.indexOf(callback); - 1 != i && disabled.splice(i, 1) - } - handlers[eventName] = callback - }, EventEmitter.removeDefaultHandler = function(eventName, callback) { - var handlers = this._defaultHandlers; - if (handlers) { - var disabled = handlers._disabled_[eventName]; - if (handlers[eventName] == callback) handlers[eventName], disabled && this.setDefaultHandler(eventName, disabled.pop()); - else if (disabled) { - var i = disabled.indexOf(callback); - 1 != i && disabled.splice(i, 1) - } - } - }, EventEmitter.on = EventEmitter.addEventListener = function(eventName, callback, capturing) { - this._eventRegistry = this._eventRegistry || {}; - var listeners = this._eventRegistry[eventName]; - return listeners || (listeners = this._eventRegistry[eventName] = []), -1 == listeners.indexOf(callback) && listeners[capturing ? "unshift" : "push"](callback), callback - }, EventEmitter.off = EventEmitter.removeListener = EventEmitter.removeEventListener = function(eventName, callback) { - this._eventRegistry = this._eventRegistry || {}; - var listeners = this._eventRegistry[eventName]; - if (listeners) { - var index = listeners.indexOf(callback); - 1 !== index && listeners.splice(index, 1) - } - }, EventEmitter.removeAllListeners = function(eventName) { - this._eventRegistry && (this._eventRegistry[eventName] = []) - }, exports.EventEmitter = EventEmitter -}), ace.define("ace/anchor", ["require", "exports", "module", "ace/lib/oop", "ace/lib/event_emitter"], function(acequire, exports) { - "use strict"; - var oop = acequire("./lib/oop"), - EventEmitter = acequire("./lib/event_emitter").EventEmitter, - Anchor = exports.Anchor = function(doc, row, column) { - this.$onChange = this.onChange.bind(this), this.attach(doc), column === void 0 ? this.setPosition(row.row, row.column) : this.setPosition(row, column) - }; - (function() { - function $pointsInOrder(point1, point2, equalPointsInOrder) { - var bColIsAfter = equalPointsInOrder ? point1.column <= point2.column : point1.column < point2.column; - return point1.row < point2.row || point1.row == point2.row && bColIsAfter - } - - function $getTransformedPoint(delta, point, moveIfEqual) { - var deltaIsInsert = "insert" == delta.action, - deltaRowShift = (deltaIsInsert ? 1 : -1) * (delta.end.row - delta.start.row), - deltaColShift = (deltaIsInsert ? 1 : -1) * (delta.end.column - delta.start.column), - deltaStart = delta.start, - deltaEnd = deltaIsInsert ? deltaStart : delta.end; - return $pointsInOrder(point, deltaStart, moveIfEqual) ? { - row: point.row, - column: point.column - } : $pointsInOrder(deltaEnd, point, !moveIfEqual) ? { - row: point.row + deltaRowShift, - column: point.column + (point.row == deltaEnd.row ? deltaColShift : 0) - } : { - row: deltaStart.row, - column: deltaStart.column - } - } - oop.implement(this, EventEmitter), this.getPosition = function() { - return this.$clipPositionToDocument(this.row, this.column) - }, this.getDocument = function() { - return this.document - }, this.$insertRight = !1, this.onChange = function(delta) { - if (!(delta.start.row == delta.end.row && delta.start.row != this.row || delta.start.row > this.row)) { - var point = $getTransformedPoint(delta, { - row: this.row, - column: this.column - }, this.$insertRight); - this.setPosition(point.row, point.column, !0) - } - }, this.setPosition = function(row, column, noClip) { - var pos; - if (pos = noClip ? { - row: row, - column: column - } : this.$clipPositionToDocument(row, column), this.row != pos.row || this.column != pos.column) { - var old = { - row: this.row, - column: this.column - }; - this.row = pos.row, this.column = pos.column, this._signal("change", { - old: old, - value: pos - }) - } - }, this.detach = function() { - this.document.removeEventListener("change", this.$onChange) - }, this.attach = function(doc) { - this.document = doc || this.document, this.document.on("change", this.$onChange) - }, this.$clipPositionToDocument = function(row, column) { - var pos = {}; - return row >= this.document.getLength() ? (pos.row = Math.max(0, this.document.getLength() - 1), pos.column = this.document.getLine(pos.row).length) : 0 > row ? (pos.row = 0, pos.column = 0) : (pos.row = row, pos.column = Math.min(this.document.getLine(pos.row).length, Math.max(0, column))), 0 > column && (pos.column = 0), pos - } - }).call(Anchor.prototype) -}), ace.define("ace/document", ["require", "exports", "module", "ace/lib/oop", "ace/apply_delta", "ace/lib/event_emitter", "ace/range", "ace/anchor"], function(acequire, exports) { - "use strict"; - var oop = acequire("./lib/oop"), - applyDelta = acequire("./apply_delta").applyDelta, - EventEmitter = acequire("./lib/event_emitter").EventEmitter, - Range = acequire("./range").Range, - Anchor = acequire("./anchor").Anchor, - Document = function(textOrLines) { - this.$lines = [""], 0 === textOrLines.length ? this.$lines = [""] : Array.isArray(textOrLines) ? this.insertMergedLines({ - row: 0, - column: 0 - }, textOrLines) : this.insert({ - row: 0, - column: 0 - }, textOrLines) - }; - (function() { - oop.implement(this, EventEmitter), this.setValue = function(text) { - var len = this.getLength() - 1; - this.remove(new Range(0, 0, len, this.getLine(len).length)), this.insert({ - row: 0, - column: 0 - }, text) - }, this.getValue = function() { - return this.getAllLines().join(this.getNewLineCharacter()) - }, this.createAnchor = function(row, column) { - return new Anchor(this, row, column) - }, this.$split = 0 === "aaa".split(/a/).length ? function(text) { - return text.replace(/\r\n|\r/g, "\n").split("\n"); - } : function(text) { - return text.split(/\r\n|\r|\n/); - }, this.$detectNewLine = function(text) { - var match = text.match(/^.*?(\r\n|\r|\n)/m); - this.$autoNewLine = match ? match[1] : "\n", this._signal("changeNewLineMode") - }, this.getNewLineCharacter = function() { - switch (this.$newLineMode) { - case "windows": - return "\r\n"; - case "unix": - return "\n"; - default: - return this.$autoNewLine || "\n" - } - }, this.$autoNewLine = "", this.$newLineMode = "auto", this.setNewLineMode = function(newLineMode) { - this.$newLineMode !== newLineMode && (this.$newLineMode = newLineMode, this._signal("changeNewLineMode")) - }, this.getNewLineMode = function() { - return this.$newLineMode - }, this.isNewLine = function(text) { - return "\r\n" == text || "\r" == text || "\n" == text - }, this.getLine = function(row) { - return this.$lines[row] || "" - }, this.getLines = function(firstRow, lastRow) { - return this.$lines.slice(firstRow, lastRow + 1) - }, this.getAllLines = function() { - return this.getLines(0, this.getLength()) - }, this.getLength = function() { - return this.$lines.length - }, this.getTextRange = function(range) { - return this.getLinesForRange(range).join(this.getNewLineCharacter()) - }, this.getLinesForRange = function(range) { - var lines; - if (range.start.row === range.end.row) lines = [this.getLine(range.start.row).substring(range.start.column, range.end.column)]; - else { - lines = this.getLines(range.start.row, range.end.row), lines[0] = (lines[0] || "").substring(range.start.column); - var l = lines.length - 1; - range.end.row - range.start.row == l && (lines[l] = lines[l].substring(0, range.end.column)) - } - return lines - }, this.insertLines = function(row, lines) { - return console.warn("Use of document.insertLines is deprecated. Use the insertFullLines method instead."), this.insertFullLines(row, lines) - }, this.removeLines = function(firstRow, lastRow) { - return console.warn("Use of document.removeLines is deprecated. Use the removeFullLines method instead."), this.removeFullLines(firstRow, lastRow) - }, this.insertNewLine = function(position) { - return console.warn("Use of document.insertNewLine is deprecated. Use insertMergedLines(position, ['', '']) instead."), this.insertMergedLines(position, ["", ""]) - }, this.insert = function(position, text) { - return 1 >= this.getLength() && this.$detectNewLine(text), this.insertMergedLines(position, this.$split(text)) - }, this.insertInLine = function(position, text) { - var start = this.clippedPos(position.row, position.column), - end = this.pos(position.row, position.column + text.length); - return this.applyDelta({ - start: start, - end: end, - action: "insert", - lines: [text] - }, !0), this.clonePos(end) - }, this.clippedPos = function(row, column) { - var length = this.getLength(); - void 0 === row ? row = length : 0 > row ? row = 0 : row >= length && (row = length - 1, column = void 0); - var line = this.getLine(row); - return void 0 == column && (column = line.length), column = Math.min(Math.max(column, 0), line.length), { - row: row, - column: column - } - }, this.clonePos = function(pos) { - return { - row: pos.row, - column: pos.column - } - }, this.pos = function(row, column) { - return { - row: row, - column: column - } - }, this.$clipPosition = function(position) { - var length = this.getLength(); - return position.row >= length ? (position.row = Math.max(0, length - 1), position.column = this.getLine(length - 1).length) : (position.row = Math.max(0, position.row), position.column = Math.min(Math.max(position.column, 0), this.getLine(position.row).length)), position - }, this.insertFullLines = function(row, lines) { - row = Math.min(Math.max(row, 0), this.getLength()); - var column = 0; - this.getLength() > row ? (lines = lines.concat([""]), column = 0) : (lines = [""].concat(lines), row--, column = this.$lines[row].length), this.insertMergedLines({ - row: row, - column: column - }, lines) - }, this.insertMergedLines = function(position, lines) { - var start = this.clippedPos(position.row, position.column), - end = { - row: start.row + lines.length - 1, - column: (1 == lines.length ? start.column : 0) + lines[lines.length - 1].length - }; - return this.applyDelta({ - start: start, - end: end, - action: "insert", - lines: lines - }), this.clonePos(end) - }, this.remove = function(range) { - var start = this.clippedPos(range.start.row, range.start.column), - end = this.clippedPos(range.end.row, range.end.column); - return this.applyDelta({ - start: start, - end: end, - action: "remove", - lines: this.getLinesForRange({ - start: start, - end: end - }) - }), this.clonePos(start) - }, this.removeInLine = function(row, startColumn, endColumn) { - var start = this.clippedPos(row, startColumn), - end = this.clippedPos(row, endColumn); - return this.applyDelta({ - start: start, - end: end, - action: "remove", - lines: this.getLinesForRange({ - start: start, - end: end - }) - }, !0), this.clonePos(start) - }, this.removeFullLines = function(firstRow, lastRow) { - firstRow = Math.min(Math.max(0, firstRow), this.getLength() - 1), lastRow = Math.min(Math.max(0, lastRow), this.getLength() - 1); - var deleteFirstNewLine = lastRow == this.getLength() - 1 && firstRow > 0, - deleteLastNewLine = this.getLength() - 1 > lastRow, - startRow = deleteFirstNewLine ? firstRow - 1 : firstRow, - startCol = deleteFirstNewLine ? this.getLine(startRow).length : 0, - endRow = deleteLastNewLine ? lastRow + 1 : lastRow, - endCol = deleteLastNewLine ? 0 : this.getLine(endRow).length, - range = new Range(startRow, startCol, endRow, endCol), - deletedLines = this.$lines.slice(firstRow, lastRow + 1); - return this.applyDelta({ - start: range.start, - end: range.end, - action: "remove", - lines: this.getLinesForRange(range) - }), deletedLines - }, this.removeNewLine = function(row) { - this.getLength() - 1 > row && row >= 0 && this.applyDelta({ - start: this.pos(row, this.getLine(row).length), - end: this.pos(row + 1, 0), - action: "remove", - lines: ["", ""] - }) - }, this.replace = function(range, text) { - if (range instanceof Range || (range = Range.fromPoints(range.start, range.end)), 0 === text.length && range.isEmpty()) return range.start; - if (text == this.getTextRange(range)) return range.end; - this.remove(range); - var end; - return end = text ? this.insert(range.start, text) : range.start - }, this.applyDeltas = function(deltas) { - for (var i = 0; deltas.length > i; i++) this.applyDelta(deltas[i]) - }, this.revertDeltas = function(deltas) { - for (var i = deltas.length - 1; i >= 0; i--) this.revertDelta(deltas[i]) - }, this.applyDelta = function(delta, doNotValidate) { - var isInsert = "insert" == delta.action; - (isInsert ? 1 >= delta.lines.length && !delta.lines[0] : !Range.comparePoints(delta.start, delta.end)) || (isInsert && delta.lines.length > 2e4 && this.$splitAndapplyLargeDelta(delta, 2e4), applyDelta(this.$lines, delta, doNotValidate), this._signal("change", delta)) - }, this.$splitAndapplyLargeDelta = function(delta, MAX) { - for (var lines = delta.lines, l = lines.length, row = delta.start.row, column = delta.start.column, from = 0, to = 0;;) { - from = to, to += MAX - 1; - var chunk = lines.slice(from, to); - if (to > l) { - delta.lines = chunk, delta.start.row = row + from, delta.start.column = column; - break - } - chunk.push(""), this.applyDelta({ - start: this.pos(row + from, column), - end: this.pos(row + to, column = 0), - action: delta.action, - lines: chunk - }, !0) - } - }, this.revertDelta = function(delta) { - this.applyDelta({ - start: this.clonePos(delta.start), - end: this.clonePos(delta.end), - action: "insert" == delta.action ? "remove" : "insert", - lines: delta.lines.slice() - }) - }, this.indexToPosition = function(index, startRow) { - for (var lines = this.$lines || this.getAllLines(), newlineLength = this.getNewLineCharacter().length, i = startRow || 0, l = lines.length; l > i; i++) - if (index -= lines[i].length + newlineLength, 0 > index) return { - row: i, - column: index + lines[i].length + newlineLength - }; - return { - row: l - 1, - column: lines[l - 1].length - } - }, this.positionToIndex = function(pos, startRow) { - for (var lines = this.$lines || this.getAllLines(), newlineLength = this.getNewLineCharacter().length, index = 0, row = Math.min(pos.row, lines.length), i = startRow || 0; row > i; ++i) index += lines[i].length + newlineLength; - return index + pos.column - } - }).call(Document.prototype), exports.Document = Document -}), ace.define("ace/lib/lang", ["require", "exports", "module"], function(acequire, exports) { - "use strict"; - exports.last = function(a) { - return a[a.length - 1] - }, exports.stringReverse = function(string) { - return string.split("").reverse().join("") - }, exports.stringRepeat = function(string, count) { - for (var result = ""; count > 0;) 1 & count && (result += string), (count >>= 1) && (string += string); - return result - }; - var trimBeginRegexp = /^\s\s*/, - trimEndRegexp = /\s\s*$/; - exports.stringTrimLeft = function(string) { - return string.replace(trimBeginRegexp, "") - }, exports.stringTrimRight = function(string) { - return string.replace(trimEndRegexp, "") - }, exports.copyObject = function(obj) { - var copy = {}; - for (var key in obj) copy[key] = obj[key]; - return copy - }, exports.copyArray = function(array) { - for (var copy = [], i = 0, l = array.length; l > i; i++) copy[i] = array[i] && "object" == typeof array[i] ? this.copyObject(array[i]) : array[i]; - return copy - }, exports.deepCopy = function deepCopy(obj) { - if ("object" != typeof obj || !obj) return obj; - var copy; - if (Array.isArray(obj)) { - copy = []; - for (var key = 0; obj.length > key; key++) copy[key] = deepCopy(obj[key]); - return copy - } - if ("[object Object]" !== Object.prototype.toString.call(obj)) return obj; - copy = {}; - for (var key in obj) copy[key] = deepCopy(obj[key]); - return copy - }, exports.arrayToMap = function(arr) { - for (var map = {}, i = 0; arr.length > i; i++) map[arr[i]] = 1; - return map - }, exports.createMap = function(props) { - var map = Object.create(null); - for (var i in props) map[i] = props[i]; - return map - }, exports.arrayRemove = function(array, value) { - for (var i = 0; array.length >= i; i++) value === array[i] && array.splice(i, 1) - }, exports.escapeRegExp = function(str) { - return str.replace(/([.*+?^${}()|[\]\/\\])/g, "\\$1"); - }, exports.escapeHTML = function(str) { - return str.replace(/&/g, "&").replace(/"/g, """).replace(/'/g, "'").replace(/ i; i += 2) { - if (Array.isArray(data[i + 1])) var d = { - action: "insert", - start: data[i], - lines: data[i + 1] - }; - else var d = { - action: "remove", - start: data[i], - end: data[i + 1] - }; - doc.applyDelta(d, !0) - } - return _self.$timeout ? deferredUpdate.schedule(_self.$timeout) : (_self.onUpdate(), void 0) - }) - }; - (function() { - this.$timeout = 500, this.setTimeout = function(timeout) { - this.$timeout = timeout - }, this.setValue = function(value) { - this.doc.setValue(value), this.deferredUpdate.schedule(this.$timeout) - }, this.getValue = function(callbackId) { - this.sender.callback(this.doc.getValue(), callbackId) - }, this.onUpdate = function() {}, this.isPending = function() { - return this.deferredUpdate.isPending() - } - }).call(Mirror.prototype) -}), ace.define("ace/mode/json/json_parse", ["require", "exports", "module"], function() { - "use strict"; - var at, ch, text, value, escapee = { - '"': '"', - "\\": "\\", - "/": "/", - b: "\b", - f: "\f", - n: "\n", - r: "\r", - t: " " - }, - error = function(m) { - throw { - name: "SyntaxError", - message: m, - at: at, - text: text - } - }, - reset = function (newAt) { - ch = text.charAt(newAt); - at = newAt + 1; - }, - next = function(c) { - return c && c !== ch && error("Expected '" + c + "' instead of '" + ch + "'"), ch = text.charAt(at), at += 1, ch - }, - nextUpTo = function (upTo, errorMessage) { - let currentAt = at, - i = text.indexOf(upTo, currentAt); - if (i < 0) { - error(errorMessage || 'Expected \'' + upTo + '\''); - } - reset(i + upTo.length); - return text.substring(currentAt, i); - }, - peek = function (c) { - return text.substr(at, c.length) === c; // nocommit - double check - }, - number = function() { - var number, string = ""; - for ("-" === ch && (string = "-", next("-")); ch >= "0" && "9" >= ch;) string += ch, next(); - if ("." === ch) - for (string += "."; next() && ch >= "0" && "9" >= ch;) string += ch; - if ("e" === ch || "E" === ch) - for (string += ch, next(), ("-" === ch || "+" === ch) && (string += ch, next()); ch >= "0" && "9" >= ch;) string += ch, next(); - return number = +string, isNaN(number) ? (error("Bad number"), void 0) : number - }, - string = function() { - var hex, i, uffff, string = ""; - if ('"' === ch) { - if (peek('""')) { - // literal - next('"'); - next('"'); - return nextUpTo('"""', 'failed to find closing \'"""\''); - } else { - for (; next();) { - if ('"' === ch) return next(), string; - if ("\\" === ch) - if (next(), "u" === ch) { - for (uffff = 0, i = 0; 4 > i && (hex = parseInt(next(), 16), isFinite(hex)); i += 1) uffff = 16 * uffff + hex; - string += String.fromCharCode(uffff) - } else { - if ("string" != typeof escapee[ch]) break; - string += escapee[ch] - } - else string += ch - } - } - } - error("Bad string") - }, - white = function() { - for (; ch && " " >= ch;) next() - }, - word = function() { - switch (ch) { - case "t": - return next("t"), next("r"), next("u"), next("e"), !0; - case "f": - return next("f"), next("a"), next("l"), next("s"), next("e"), !1; - case "n": - return next("n"), next("u"), next("l"), next("l"), null - } - error("Unexpected '" + ch + "'") - }, - array = function() { - var array = []; - if ("[" === ch) { - if (next("["), white(), "]" === ch) return next("]"), array; - for (; ch;) { - if (array.push(value()), white(), "]" === ch) return next("]"), array; - next(","), white() - } - } - error("Bad array") - }, - object = function() { - var key, object = {}; - if ("{" === ch) { - if (next("{"), white(), "}" === ch) return next("}"), object; - for (; ch;) { - if (key = string(), white(), next(":"), Object.hasOwnProperty.call(object, key) && error('Duplicate key "' + key + '"'), object[key] = value(), white(), "}" === ch) return next("}"), object; - next(","), white() - } - } - error("Bad object") - }; - return value = function() { - switch (white(), ch) { - case "{": - return object(); - case "[": - return array(); - case '"': - return string(); - case "-": - return number(); - default: - return ch >= "0" && "9" >= ch ? number() : word() - } - }, - function(source, reviver) { - var result; - return text = source, at = 0, ch = " ", result = value(), white(), ch && error("Syntax error"), "function" == typeof reviver ? function walk(holder, key) { - var k, v, value = holder[key]; - if (value && "object" == typeof value) - for (k in value) Object.hasOwnProperty.call(value, k) && (v = walk(value, k), void 0 !== v ? value[k] = v : delete value[k]); - return reviver.call(holder, key, value) - }({ - "": result - }, "") : result - } -}), ace.define("ace/mode/json_worker", ["require", "exports", "module", "ace/lib/oop", "ace/worker/mirror", "ace/mode/json/json_parse"], function(acequire, exports) { - "use strict"; - var oop = acequire("../lib/oop"), - Mirror = acequire("../worker/mirror").Mirror, - parse = acequire("./json/json_parse"), - JsonWorker = exports.JsonWorker = function(sender) { - Mirror.call(this, sender), this.setTimeout(200) - }; - oop.inherits(JsonWorker, Mirror), - function() { - this.onUpdate = function() { - var value = this.doc.getValue(), - errors = []; - try { - value && parse(value) - } catch (e) { - var pos = this.doc.indexToPosition(e.at - 1); - errors.push({ - row: pos.row, - column: pos.column, - text: e.message, - type: "error" - }) - } - this.sender.emit("annotate", errors) - } - }.call(JsonWorker.prototype) -}), ace.define("ace/lib/es5-shim", ["require", "exports", "module"], function() { - function Empty() {} - - function doesDefinePropertyWork(object) { - try { - return Object.defineProperty(object, "sentinel", {}), "sentinel" in object - } catch (exception) {} - } - - function toInteger(n) { - return n = +n, n !== n ? n = 0 : 0 !== n && n !== 1 / 0 && n !== -(1 / 0) && (n = (n > 0 || -1) * Math.floor(Math.abs(n))), n - } - Function.prototype.bind || (Function.prototype.bind = function(that) { - var target = this; - if ("function" != typeof target) throw new TypeError("Function.prototype.bind called on incompatible " + target); - var args = slice.call(arguments, 1), - bound = function() { - if (this instanceof bound) { - var result = target.apply(this, args.concat(slice.call(arguments))); - return Object(result) === result ? result : this - } - return target.apply(that, args.concat(slice.call(arguments))) - }; - return target.prototype && (Empty.prototype = target.prototype, bound.prototype = new Empty, Empty.prototype = null), bound - }); - var defineGetter, defineSetter, lookupGetter, lookupSetter, supportsAccessors, call = Function.prototype.call, - prototypeOfArray = Array.prototype, - prototypeOfObject = Object.prototype, - slice = prototypeOfArray.slice, - _toString = call.bind(prototypeOfObject.toString), - owns = call.bind(prototypeOfObject.hasOwnProperty); - if ((supportsAccessors = owns(prototypeOfObject, "__defineGetter__")) && (defineGetter = call.bind(prototypeOfObject.__defineGetter__), defineSetter = call.bind(prototypeOfObject.__defineSetter__), lookupGetter = call.bind(prototypeOfObject.__lookupGetter__), lookupSetter = call.bind(prototypeOfObject.__lookupSetter__)), 2 != [1, 2].splice(0).length) - if (function() { - function makeArray(l) { - var a = Array(l + 2); - return a[0] = a[1] = 0, a - } - var lengthBefore, array = []; - return array.splice.apply(array, makeArray(20)), array.splice.apply(array, makeArray(26)), lengthBefore = array.length, array.splice(5, 0, "XXX"), lengthBefore + 1 == array.length, lengthBefore + 1 == array.length ? !0 : void 0 - }()) { - var array_splice = Array.prototype.splice; - Array.prototype.splice = function(start, deleteCount) { - return arguments.length ? array_splice.apply(this, [void 0 === start ? 0 : start, void 0 === deleteCount ? this.length - start : deleteCount].concat(slice.call(arguments, 2))) : [] - } - } else Array.prototype.splice = function(pos, removeCount) { - var length = this.length; - pos > 0 ? pos > length && (pos = length) : void 0 == pos ? pos = 0 : 0 > pos && (pos = Math.max(length + pos, 0)), length > pos + removeCount || (removeCount = length - pos); - var removed = this.slice(pos, pos + removeCount), - insert = slice.call(arguments, 2), - add = insert.length; - if (pos === length) add && this.push.apply(this, insert); - else { - var remove = Math.min(removeCount, length - pos), - tailOldPos = pos + remove, - tailNewPos = tailOldPos + add - remove, - tailCount = length - tailOldPos, - lengthAfterRemove = length - remove; - if (tailOldPos > tailNewPos) - for (var i = 0; tailCount > i; ++i) this[tailNewPos + i] = this[tailOldPos + i]; - else if (tailNewPos > tailOldPos) - for (i = tailCount; i--;) this[tailNewPos + i] = this[tailOldPos + i]; - if (add && pos === lengthAfterRemove) this.length = lengthAfterRemove, this.push.apply(this, insert); - else - for (this.length = lengthAfterRemove + add, i = 0; add > i; ++i) this[pos + i] = insert[i] - } - return removed - }; - Array.isArray || (Array.isArray = function(obj) { - return "[object Array]" == _toString(obj) - }); - var boxedString = Object("a"), - splitString = "a" != boxedString[0] || !(0 in boxedString); - if (Array.prototype.forEach || (Array.prototype.forEach = function(fun) { - var object = toObject(this), - self = splitString && "[object String]" == _toString(this) ? this.split("") : object, - thisp = arguments[1], - i = -1, - length = self.length >>> 0; - if ("[object Function]" != _toString(fun)) throw new TypeError; - for (; length > ++i;) i in self && fun.call(thisp, self[i], i, object) - }), Array.prototype.map || (Array.prototype.map = function(fun) { - var object = toObject(this), - self = splitString && "[object String]" == _toString(this) ? this.split("") : object, - length = self.length >>> 0, - result = Array(length), - thisp = arguments[1]; - if ("[object Function]" != _toString(fun)) throw new TypeError(fun + " is not a function"); - for (var i = 0; length > i; i++) i in self && (result[i] = fun.call(thisp, self[i], i, object)); - return result - }), Array.prototype.filter || (Array.prototype.filter = function(fun) { - var value, object = toObject(this), - self = splitString && "[object String]" == _toString(this) ? this.split("") : object, - length = self.length >>> 0, - result = [], - thisp = arguments[1]; - if ("[object Function]" != _toString(fun)) throw new TypeError(fun + " is not a function"); - for (var i = 0; length > i; i++) i in self && (value = self[i], fun.call(thisp, value, i, object) && result.push(value)); - return result - }), Array.prototype.every || (Array.prototype.every = function(fun) { - var object = toObject(this), - self = splitString && "[object String]" == _toString(this) ? this.split("") : object, - length = self.length >>> 0, - thisp = arguments[1]; - if ("[object Function]" != _toString(fun)) throw new TypeError(fun + " is not a function"); - for (var i = 0; length > i; i++) - if (i in self && !fun.call(thisp, self[i], i, object)) return !1; - return !0 - }), Array.prototype.some || (Array.prototype.some = function(fun) { - var object = toObject(this), - self = splitString && "[object String]" == _toString(this) ? this.split("") : object, - length = self.length >>> 0, - thisp = arguments[1]; - if ("[object Function]" != _toString(fun)) throw new TypeError(fun + " is not a function"); - for (var i = 0; length > i; i++) - if (i in self && fun.call(thisp, self[i], i, object)) return !0; - return !1 - }), Array.prototype.reduce || (Array.prototype.reduce = function(fun) { - var object = toObject(this), - self = splitString && "[object String]" == _toString(this) ? this.split("") : object, - length = self.length >>> 0; - if ("[object Function]" != _toString(fun)) throw new TypeError(fun + " is not a function"); - if (!length && 1 == arguments.length) throw new TypeError("reduce of empty array with no initial value"); - var result, i = 0; - if (arguments.length >= 2) result = arguments[1]; - else - for (;;) { - if (i in self) { - result = self[i++]; - break - } - if (++i >= length) throw new TypeError("reduce of empty array with no initial value") - } - for (; length > i; i++) i in self && (result = fun.call(void 0, result, self[i], i, object)); - return result - }), Array.prototype.reduceRight || (Array.prototype.reduceRight = function(fun) { - var object = toObject(this), - self = splitString && "[object String]" == _toString(this) ? this.split("") : object, - length = self.length >>> 0; - if ("[object Function]" != _toString(fun)) throw new TypeError(fun + " is not a function"); - if (!length && 1 == arguments.length) throw new TypeError("reduceRight of empty array with no initial value"); - var result, i = length - 1; - if (arguments.length >= 2) result = arguments[1]; - else - for (;;) { - if (i in self) { - result = self[i--]; - break - } - if (0 > --i) throw new TypeError("reduceRight of empty array with no initial value") - } - do i in this && (result = fun.call(void 0, result, self[i], i, object)); while (i--); - return result - }), Array.prototype.indexOf && -1 == [0, 1].indexOf(1, 2) || (Array.prototype.indexOf = function(sought) { - var self = splitString && "[object String]" == _toString(this) ? this.split("") : toObject(this), - length = self.length >>> 0; - if (!length) return -1; - var i = 0; - for (arguments.length > 1 && (i = toInteger(arguments[1])), i = i >= 0 ? i : Math.max(0, length + i); length > i; i++) - if (i in self && self[i] === sought) return i; - return -1 - }), Array.prototype.lastIndexOf && -1 == [0, 1].lastIndexOf(0, -3) || (Array.prototype.lastIndexOf = function(sought) { - var self = splitString && "[object String]" == _toString(this) ? this.split("") : toObject(this), - length = self.length >>> 0; - if (!length) return -1; - var i = length - 1; - for (arguments.length > 1 && (i = Math.min(i, toInteger(arguments[1]))), i = i >= 0 ? i : length - Math.abs(i); i >= 0; i--) - if (i in self && sought === self[i]) return i; - return -1 - }), Object.getPrototypeOf || (Object.getPrototypeOf = function(object) { - return object.__proto__ || (object.constructor ? object.constructor.prototype : prototypeOfObject) - }), !Object.getOwnPropertyDescriptor) { - var ERR_NON_OBJECT = "Object.getOwnPropertyDescriptor called on a non-object: "; - Object.getOwnPropertyDescriptor = function(object, property) { - if ("object" != typeof object && "function" != typeof object || null === object) throw new TypeError(ERR_NON_OBJECT + object); - if (owns(object, property)) { - var descriptor, getter, setter; - if (descriptor = { - enumerable: !0, - configurable: !0 - }, supportsAccessors) { - var prototype = object.__proto__; - object.__proto__ = prototypeOfObject; - var getter = lookupGetter(object, property), - setter = lookupSetter(object, property); - if (object.__proto__ = prototype, getter || setter) return getter && (descriptor.get = getter), setter && (descriptor.set = setter), descriptor - } - return descriptor.value = object[property], descriptor - } - } - } - if (Object.getOwnPropertyNames || (Object.getOwnPropertyNames = function(object) { - return Object.keys(object) - }), !Object.create) { - var createEmpty; - createEmpty = null === Object.prototype.__proto__ ? function() { - return { - __proto__: null - } - } : function() { - var empty = {}; - for (var i in empty) empty[i] = null; - return empty.constructor = empty.hasOwnProperty = empty.propertyIsEnumerable = empty.isPrototypeOf = empty.toLocaleString = empty.toString = empty.valueOf = empty.__proto__ = null, empty - }, Object.create = function(prototype, properties) { - var object; - if (null === prototype) object = createEmpty(); - else { - if ("object" != typeof prototype) throw new TypeError("typeof prototype[" + typeof prototype + "] != 'object'"); - var Type = function() {}; - Type.prototype = prototype, object = new Type, object.__proto__ = prototype - } - return void 0 !== properties && Object.defineProperties(object, properties), object - } - } - if (Object.defineProperty) { - var definePropertyWorksOnObject = doesDefinePropertyWork({}), - definePropertyWorksOnDom = "undefined" == typeof document || doesDefinePropertyWork(document.createElement("div")); - if (!definePropertyWorksOnObject || !definePropertyWorksOnDom) var definePropertyFallback = Object.defineProperty - } - if (!Object.defineProperty || definePropertyFallback) { - var ERR_NON_OBJECT_DESCRIPTOR = "Property description must be an object: ", - ERR_NON_OBJECT_TARGET = "Object.defineProperty called on non-object: ", - ERR_ACCESSORS_NOT_SUPPORTED = "getters & setters can not be defined on this javascript engine"; - Object.defineProperty = function(object, property, descriptor) { - if ("object" != typeof object && "function" != typeof object || null === object) throw new TypeError(ERR_NON_OBJECT_TARGET + object); - if ("object" != typeof descriptor && "function" != typeof descriptor || null === descriptor) throw new TypeError(ERR_NON_OBJECT_DESCRIPTOR + descriptor); - if (definePropertyFallback) try { - return definePropertyFallback.call(Object, object, property, descriptor) - } catch (exception) {} - if (owns(descriptor, "value")) - if (supportsAccessors && (lookupGetter(object, property) || lookupSetter(object, property))) { - var prototype = object.__proto__; - object.__proto__ = prototypeOfObject, delete object[property], object[property] = descriptor.value, object.__proto__ = prototype - } else object[property] = descriptor.value; - else { - if (!supportsAccessors) throw new TypeError(ERR_ACCESSORS_NOT_SUPPORTED); - owns(descriptor, "get") && defineGetter(object, property, descriptor.get), owns(descriptor, "set") && defineSetter(object, property, descriptor.set) - } - return object - } - } - Object.defineProperties || (Object.defineProperties = function(object, properties) { - for (var property in properties) owns(properties, property) && Object.defineProperty(object, property, properties[property]); - return object - }), Object.seal || (Object.seal = function(object) { - return object - }), Object.freeze || (Object.freeze = function(object) { - return object - }); - try { - Object.freeze(function() {}) - } catch (exception) { - Object.freeze = function(freezeObject) { - return function(object) { - return "function" == typeof object ? object : freezeObject(object) - } - }(Object.freeze) - } - if (Object.preventExtensions || (Object.preventExtensions = function(object) { - return object - }), Object.isSealed || (Object.isSealed = function() { - return !1 - }), Object.isFrozen || (Object.isFrozen = function() { - return !1 - }), Object.isExtensible || (Object.isExtensible = function(object) { - if (Object(object) === object) throw new TypeError; - for (var name = ""; owns(object, name);) name += "?"; - object[name] = !0; - var returnValue = owns(object, name); - return delete object[name], returnValue - }), !Object.keys) { - var hasDontEnumBug = !0, - dontEnums = ["toString", "toLocaleString", "valueOf", "hasOwnProperty", "isPrototypeOf", "propertyIsEnumerable", "constructor"], - dontEnumsLength = dontEnums.length; - for (var key in { - toString: null - }) hasDontEnumBug = !1; - Object.keys = function(object) { - if ("object" != typeof object && "function" != typeof object || null === object) throw new TypeError("Object.keys called on a non-object"); - var keys = []; - for (var name in object) owns(object, name) && keys.push(name); - if (hasDontEnumBug) - for (var i = 0, ii = dontEnumsLength; ii > i; i++) { - var dontEnum = dontEnums[i]; - owns(object, dontEnum) && keys.push(dontEnum) - } - return keys - } - } - Date.now || (Date.now = function() { - return (new Date).getTime() - }); - var ws = " \n \f\r   ᠎              \u2028\u2029"; - if (!String.prototype.trim || ws.trim()) { - ws = "[" + ws + "]"; - var trimBeginRegexp = RegExp("^" + ws + ws + "*"), - trimEndRegexp = RegExp(ws + ws + "*$"); - String.prototype.trim = function() { - return (this + "").replace(trimBeginRegexp, "").replace(trimEndRegexp, "") - } - } - var toObject = function(o) { - if (null == o) throw new TypeError("can't convert " + o + " to object"); - return Object(o) - } -}); diff --git a/packages/kbn-ace/src/ace/modes/x_json/x_json.ts b/packages/kbn-ace/src/ace/modes/x_json/x_json.ts deleted file mode 100644 index 5a535e237a327..0000000000000 --- a/packages/kbn-ace/src/ace/modes/x_json/x_json.ts +++ /dev/null @@ -1,57 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the "Elastic License - * 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side - * Public License v 1"; you may not use this file except in compliance with, at - * your election, the "Elastic License 2.0", the "GNU Affero General Public - * License v3.0 only", or the "Server Side Public License, v 1". - */ - -import ace from 'brace'; -import { XJsonHighlightRules } from '..'; -import { workerModule } from './worker'; - -const { WorkerClient } = ace.acequire('ace/worker/worker_client'); - -const oop = ace.acequire('ace/lib/oop'); - -const { Mode: JSONMode } = ace.acequire('ace/mode/json'); -const { Tokenizer: AceTokenizer } = ace.acequire('ace/tokenizer'); -const { MatchingBraceOutdent } = ace.acequire('ace/mode/matching_brace_outdent'); -const { CstyleBehaviour } = ace.acequire('ace/mode/behaviour/cstyle'); -const { FoldMode: CStyleFoldMode } = ace.acequire('ace/mode/folding/cstyle'); - -const XJsonMode: any = function XJsonMode(this: any) { - const ruleset: any = new (XJsonHighlightRules as any)(); - ruleset.normalizeRules(); - this.$tokenizer = new AceTokenizer(ruleset.getRules()); - this.$outdent = new MatchingBraceOutdent(); - this.$behaviour = new CstyleBehaviour(); - this.foldingRules = new CStyleFoldMode(); -}; - -oop.inherits(XJsonMode, JSONMode); - -// Then clobber `createWorker` method to install our worker source. Per ace's wiki: https://github.com/ajaxorg/ace/wiki/Syntax-validation -(XJsonMode.prototype as any).createWorker = function (session: ace.IEditSession) { - const xJsonWorker = new WorkerClient(['ace'], workerModule, 'JsonWorker'); - - xJsonWorker.attachToDocument(session.getDocument()); - - xJsonWorker.on('annotate', function (e: { data: any }) { - session.setAnnotations(e.data); - }); - - xJsonWorker.on('terminate', function () { - session.clearAnnotations(); - }); - - return xJsonWorker; -}; - -export { XJsonMode }; - -export function installXJsonMode(editor: ace.Editor) { - const session = editor.getSession(); - session.setMode(new XJsonMode()); -} diff --git a/packages/kbn-ace/tsconfig.json b/packages/kbn-ace/tsconfig.json deleted file mode 100644 index a545abd7d65a6..0000000000000 --- a/packages/kbn-ace/tsconfig.json +++ /dev/null @@ -1,16 +0,0 @@ -{ - "extends": "../../tsconfig.base.json", - "compilerOptions": { - "allowJs": false, - "outDir": "target/types", - "stripInternal": true, - "types": ["node"] - }, - "include": [ - "**/*.ts", - "src/ace/modes/x_json/worker/x_json.ace.worker.js" - ], - "exclude": [ - "target/**/*", - ] -} diff --git a/packages/kbn-alerting-types/index.ts b/packages/kbn-alerting-types/index.ts index 0a930e6a9319c..b2288900a1248 100644 --- a/packages/kbn-alerting-types/index.ts +++ b/packages/kbn-alerting-types/index.ts @@ -18,4 +18,5 @@ export * from './r_rule_types'; export * from './rule_notify_when_type'; export * from './rule_type_types'; export * from './rule_types'; +export * from './rule_settings'; export * from './search_strategy_types'; diff --git a/packages/kbn-alerting-types/rule_settings.ts b/packages/kbn-alerting-types/rule_settings.ts new file mode 100644 index 0000000000000..b25ad201c2dc0 --- /dev/null +++ b/packages/kbn-alerting-types/rule_settings.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", the "GNU Affero General Public License v3.0 only", and the "Server Side + * Public License v 1"; you may not use this file except in compliance with, at + * your election, the "Elastic License 2.0", the "GNU Affero General Public + * License v3.0 only", or the "Server Side Public License, v 1". + */ + +export interface RulesSettingsModificationMetadata { + createdBy: string | null; + updatedBy: string | null; + createdAt: string; + updatedAt: string; +} + +export interface RulesSettingsFlappingProperties { + enabled: boolean; + lookBackWindow: number; + statusChangeThreshold: number; +} + +export interface RuleSpecificFlappingProperties { + lookBackWindow: number; + statusChangeThreshold: number; +} + +export type RulesSettingsFlapping = RulesSettingsFlappingProperties & + RulesSettingsModificationMetadata; + +export interface RulesSettingsQueryDelayProperties { + delay: number; +} + +export type RulesSettingsQueryDelay = RulesSettingsQueryDelayProperties & + RulesSettingsModificationMetadata; + +export interface RulesSettingsProperties { + flapping?: RulesSettingsFlappingProperties; + queryDelay?: RulesSettingsQueryDelayProperties; +} + +export interface RulesSettings { + flapping?: RulesSettingsFlapping; + queryDelay?: RulesSettingsQueryDelay; +} diff --git a/packages/kbn-alerts-ui-shared/src/common/apis/fetch_flapping_settings/fetch_flapping_settings.test.ts b/packages/kbn-alerts-ui-shared/src/common/apis/fetch_flapping_settings/fetch_flapping_settings.test.ts new file mode 100644 index 0000000000000..d5feaa731335a --- /dev/null +++ b/packages/kbn-alerts-ui-shared/src/common/apis/fetch_flapping_settings/fetch_flapping_settings.test.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", the "GNU Affero General Public License v3.0 only", and the "Server Side + * Public License v 1"; you may not use this file except in compliance with, at + * your election, the "Elastic License 2.0", the "GNU Affero General Public + * License v3.0 only", or the "Server Side Public License, v 1". + */ + +import { httpServiceMock } from '@kbn/core/public/mocks'; +import { fetchFlappingSettings } from './fetch_flapping_settings'; + +const http = httpServiceMock.createStartContract(); + +describe('fetchFlappingSettings', () => { + beforeEach(() => { + jest.resetAllMocks(); + }); + + test('should call fetch rule flapping API', async () => { + const now = new Date().toISOString(); + http.get.mockResolvedValue({ + created_by: 'test', + updated_by: 'test', + created_at: now, + updated_at: now, + enabled: true, + look_back_window: 20, + status_change_threshold: 20, + }); + + const result = await fetchFlappingSettings({ http }); + + expect(result).toEqual({ + createdBy: 'test', + updatedBy: 'test', + createdAt: now, + updatedAt: now, + enabled: true, + lookBackWindow: 20, + statusChangeThreshold: 20, + }); + }); +}); diff --git a/packages/kbn-alerts-ui-shared/src/common/apis/fetch_flapping_settings/fetch_flapping_settings.ts b/packages/kbn-alerts-ui-shared/src/common/apis/fetch_flapping_settings/fetch_flapping_settings.ts new file mode 100644 index 0000000000000..6ad702ebc945e --- /dev/null +++ b/packages/kbn-alerts-ui-shared/src/common/apis/fetch_flapping_settings/fetch_flapping_settings.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", the "GNU Affero General Public License v3.0 only", and the "Server Side + * Public License v 1"; you may not use this file except in compliance with, at + * your election, the "Elastic License 2.0", the "GNU Affero General Public + * License v3.0 only", or the "Server Side Public License, v 1". + */ + +import { HttpSetup } from '@kbn/core/public'; +import { AsApiContract } from '@kbn/actions-types'; +import { RulesSettingsFlapping } from '@kbn/alerting-types'; +import { INTERNAL_BASE_ALERTING_API_PATH } from '../../constants'; +import { transformFlappingSettingsResponse } from './transform_flapping_settings_response'; + +export const fetchFlappingSettings = async ({ http }: { http: HttpSetup }) => { + const res = await http.get>( + `${INTERNAL_BASE_ALERTING_API_PATH}/rules/settings/_flapping` + ); + return transformFlappingSettingsResponse(res); +}; diff --git a/src/plugins/console/public/lib/ace_token_provider/index.ts b/packages/kbn-alerts-ui-shared/src/common/apis/fetch_flapping_settings/index.ts similarity index 91% rename from src/plugins/console/public/lib/ace_token_provider/index.ts rename to packages/kbn-alerts-ui-shared/src/common/apis/fetch_flapping_settings/index.ts index 8819ac19a1262..68ff193255403 100644 --- a/src/plugins/console/public/lib/ace_token_provider/index.ts +++ b/packages/kbn-alerts-ui-shared/src/common/apis/fetch_flapping_settings/index.ts @@ -7,4 +7,4 @@ * License v3.0 only", or the "Server Side Public License, v 1". */ -export * from './token_provider'; +export * from './fetch_flapping_settings'; diff --git a/packages/kbn-alerts-ui-shared/src/common/apis/fetch_flapping_settings/transform_flapping_settings_response.test.ts b/packages/kbn-alerts-ui-shared/src/common/apis/fetch_flapping_settings/transform_flapping_settings_response.test.ts new file mode 100644 index 0000000000000..e53d133f6838b --- /dev/null +++ b/packages/kbn-alerts-ui-shared/src/common/apis/fetch_flapping_settings/transform_flapping_settings_response.test.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", the "GNU Affero General Public License v3.0 only", and the "Server Side + * Public License v 1"; you may not use this file except in compliance with, at + * your election, the "Elastic License 2.0", the "GNU Affero General Public + * License v3.0 only", or the "Server Side Public License, v 1". + */ + +import { transformFlappingSettingsResponse } from './transform_flapping_settings_response'; + +describe('transformFlappingSettingsResponse', () => { + test('should transform flapping settings response', () => { + const now = new Date().toISOString(); + + const result = transformFlappingSettingsResponse({ + created_by: 'test', + updated_by: 'test', + created_at: now, + updated_at: now, + enabled: true, + look_back_window: 20, + status_change_threshold: 20, + }); + + expect(result).toEqual({ + createdBy: 'test', + updatedBy: 'test', + createdAt: now, + updatedAt: now, + enabled: true, + lookBackWindow: 20, + statusChangeThreshold: 20, + }); + }); +}); diff --git a/packages/kbn-alerts-ui-shared/src/common/apis/fetch_flapping_settings/transform_flapping_settings_response.ts b/packages/kbn-alerts-ui-shared/src/common/apis/fetch_flapping_settings/transform_flapping_settings_response.ts new file mode 100644 index 0000000000000..a628829927a3b --- /dev/null +++ b/packages/kbn-alerts-ui-shared/src/common/apis/fetch_flapping_settings/transform_flapping_settings_response.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", the "GNU Affero General Public License v3.0 only", and the "Server Side + * Public License v 1"; you may not use this file except in compliance with, at + * your election, the "Elastic License 2.0", the "GNU Affero General Public + * License v3.0 only", or the "Server Side Public License, v 1". + */ + +import { AsApiContract } from '@kbn/actions-types'; +import { RulesSettingsFlapping } from '@kbn/alerting-types'; + +export const transformFlappingSettingsResponse = ({ + look_back_window: lookBackWindow, + status_change_threshold: statusChangeThreshold, + created_at: createdAt, + created_by: createdBy, + updated_at: updatedAt, + updated_by: updatedBy, + ...rest +}: AsApiContract): RulesSettingsFlapping => ({ + ...rest, + lookBackWindow, + statusChangeThreshold, + createdAt, + createdBy, + updatedAt, + updatedBy, +}); diff --git a/src/plugins/console/public/application/models/index.ts b/packages/kbn-alerts-ui-shared/src/common/constants/rule_flapping.ts similarity index 79% rename from src/plugins/console/public/application/models/index.ts rename to packages/kbn-alerts-ui-shared/src/common/constants/rule_flapping.ts index 0d4a8f474daee..49ea5a63b3fca 100644 --- a/src/plugins/console/public/application/models/index.ts +++ b/packages/kbn-alerts-ui-shared/src/common/constants/rule_flapping.ts @@ -7,5 +7,5 @@ * License v3.0 only", or the "Server Side Public License, v 1". */ -export * from './legacy_core_editor/legacy_core_editor'; -export * from './sense_editor'; +// Feature flag for frontend rule specific flapping in rule flyout +export const IS_RULE_SPECIFIC_FLAPPING_ENABLED = false; diff --git a/packages/kbn-alerts-ui-shared/src/common/hooks/use_fetch_flapping_settings.test.tsx b/packages/kbn-alerts-ui-shared/src/common/hooks/use_fetch_flapping_settings.test.tsx new file mode 100644 index 0000000000000..10e1869b9e64c --- /dev/null +++ b/packages/kbn-alerts-ui-shared/src/common/hooks/use_fetch_flapping_settings.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", the "GNU Affero General Public License v3.0 only", and the "Server Side + * Public License v 1"; you may not use this file except in compliance with, at + * your election, the "Elastic License 2.0", the "GNU Affero General Public + * License v3.0 only", or the "Server Side Public License, v 1". + */ + +import React, { FunctionComponent } from 'react'; +import { QueryClient, QueryClientProvider } from '@tanstack/react-query'; +import { renderHook } from '@testing-library/react-hooks'; +import { testQueryClientConfig } from '../test_utils/test_query_client_config'; +import { useFetchFlappingSettings } from './use_fetch_flapping_settings'; +import { httpServiceMock } from '@kbn/core-http-browser-mocks'; + +const queryClient = new QueryClient(testQueryClientConfig); + +const wrapper: FunctionComponent> = ({ children }) => ( + {children} +); + +const http = httpServiceMock.createStartContract(); + +const now = new Date().toISOString(); + +describe('useFetchFlappingSettings', () => { + beforeEach(() => { + http.get.mockResolvedValue({ + created_by: 'test', + updated_by: 'test', + created_at: now, + updated_at: now, + enabled: true, + look_back_window: 20, + status_change_threshold: 20, + }); + }); + + afterEach(() => { + jest.resetAllMocks(); + queryClient.clear(); + }); + + test('should call fetchFlappingSettings with the correct parameters', async () => { + const { result, waitFor } = renderHook( + () => useFetchFlappingSettings({ http, enabled: true }), + { + wrapper, + } + ); + + await waitFor(() => { + return expect(result.current.isInitialLoading).toEqual(false); + }); + + expect(result.current.data).toEqual({ + createdAt: now, + createdBy: 'test', + updatedAt: now, + updatedBy: 'test', + enabled: true, + lookBackWindow: 20, + statusChangeThreshold: 20, + }); + }); + + test('should not call fetchFlappingSettings if enabled is false', async () => { + const { result, waitFor } = renderHook( + () => useFetchFlappingSettings({ http, enabled: false }), + { + wrapper, + } + ); + + await waitFor(() => { + return expect(result.current.isInitialLoading).toEqual(false); + }); + + expect(http.get).not.toHaveBeenCalled(); + }); + + test('should call onSuccess when the fetching was successful', async () => { + const onSuccessMock = jest.fn(); + const { result, waitFor } = renderHook( + () => useFetchFlappingSettings({ http, enabled: true, onSuccess: onSuccessMock }), + { + wrapper, + } + ); + + await waitFor(() => { + return expect(result.current.isInitialLoading).toEqual(false); + }); + + expect(onSuccessMock).toHaveBeenCalledWith({ + createdAt: now, + createdBy: 'test', + updatedAt: now, + updatedBy: 'test', + enabled: true, + lookBackWindow: 20, + statusChangeThreshold: 20, + }); + }); +}); diff --git a/packages/kbn-alerts-ui-shared/src/common/hooks/use_fetch_flapping_settings.ts b/packages/kbn-alerts-ui-shared/src/common/hooks/use_fetch_flapping_settings.ts new file mode 100644 index 0000000000000..6b72c2fea734b --- /dev/null +++ b/packages/kbn-alerts-ui-shared/src/common/hooks/use_fetch_flapping_settings.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", the "GNU Affero General Public License v3.0 only", and the "Server Side + * Public License v 1"; you may not use this file except in compliance with, at + * your election, the "Elastic License 2.0", the "GNU Affero General Public + * License v3.0 only", or the "Server Side Public License, v 1". + */ + +import { useQuery } from '@tanstack/react-query'; +import { HttpStart } from '@kbn/core-http-browser'; +import { RulesSettingsFlapping } from '@kbn/alerting-types/rule_settings'; +import { fetchFlappingSettings } from '../apis/fetch_flapping_settings'; + +interface UseFetchFlappingSettingsProps { + http: HttpStart; + enabled: boolean; + onSuccess?: (settings: RulesSettingsFlapping) => void; +} + +export const useFetchFlappingSettings = (props: UseFetchFlappingSettingsProps) => { + const { http, enabled, onSuccess } = props; + + const queryFn = () => { + return fetchFlappingSettings({ http }); + }; + + const { data, isFetching, isError, isLoadingError, isLoading, isInitialLoading } = useQuery({ + queryKey: ['fetchFlappingSettings'], + queryFn, + onSuccess, + enabled, + refetchOnWindowFocus: false, + retry: false, + }); + + return { + isInitialLoading, + isLoading: isLoading || isFetching, + isError: isError || isLoadingError, + data, + }; +}; diff --git a/packages/kbn-alerts-ui-shared/src/rule_form/create_rule_form.tsx b/packages/kbn-alerts-ui-shared/src/rule_form/create_rule_form.tsx index 71aeb2bcaab77..fc96ae214a7a8 100644 --- a/packages/kbn-alerts-ui-shared/src/rule_form/create_rule_form.tsx +++ b/packages/kbn-alerts-ui-shared/src/rule_form/create_rule_form.tsx @@ -92,6 +92,7 @@ export const CreateRuleForm = (props: CreateRuleFormProps) => { connectors, connectorTypes, aadTemplateFields, + flappingSettings, } = useLoadDependencies({ http, toasts: notifications.toasts, @@ -117,6 +118,7 @@ export const CreateRuleForm = (props: CreateRuleFormProps) => { actions: newFormData.actions, notifyWhen: newFormData.notifyWhen, alertDelay: newFormData.alertDelay, + flapping: newFormData.flapping, }, }); }, @@ -173,6 +175,7 @@ export const CreateRuleForm = (props: CreateRuleFormProps) => { selectedRuleTypeModel: ruleTypeModel, selectedRuleType: ruleType, validConsumers, + flappingSettings, canShowConsumerSelection, showMustacheAutocompleteSwitch, multiConsumerSelection: getInitialMultiConsumer({ diff --git a/packages/kbn-alerts-ui-shared/src/rule_form/edit_rule_form.tsx b/packages/kbn-alerts-ui-shared/src/rule_form/edit_rule_form.tsx index 5091444276873..6e92b94cc2e0d 100644 --- a/packages/kbn-alerts-ui-shared/src/rule_form/edit_rule_form.tsx +++ b/packages/kbn-alerts-ui-shared/src/rule_form/edit_rule_form.tsx @@ -69,6 +69,7 @@ export const EditRuleForm = (props: EditRuleFormProps) => { connectors, connectorTypes, aadTemplateFields, + flappingSettings, } = useLoadDependencies({ http, toasts: notifications.toasts, @@ -89,6 +90,7 @@ export const EditRuleForm = (props: EditRuleFormProps) => { actions: newFormData.actions, notifyWhen: newFormData.notifyWhen, alertDelay: newFormData.alertDelay, + flapping: newFormData.flapping, }, }); }, @@ -160,6 +162,7 @@ export const EditRuleForm = (props: EditRuleFormProps) => { minimumScheduleInterval: uiConfig?.minimumScheduleInterval, selectedRuleType: ruleType, selectedRuleTypeModel: ruleTypeModel, + flappingSettings, showMustacheAutocompleteSwitch, }} > diff --git a/packages/kbn-alerts-ui-shared/src/rule_form/hooks/use_load_dependencies.test.tsx b/packages/kbn-alerts-ui-shared/src/rule_form/hooks/use_load_dependencies.test.tsx index 263c9e2118056..9d2ce3b6f1211 100644 --- a/packages/kbn-alerts-ui-shared/src/rule_form/hooks/use_load_dependencies.test.tsx +++ b/packages/kbn-alerts-ui-shared/src/rule_form/hooks/use_load_dependencies.test.tsx @@ -50,6 +50,10 @@ jest.mock('../utils/get_authorized_rule_types', () => ({ getAvailableRuleTypes: jest.fn(), })); +jest.mock('../../common/hooks/use_fetch_flapping_settings', () => ({ + useFetchFlappingSettings: jest.fn(), +})); + const { useLoadUiConfig } = jest.requireMock('../../common/hooks/use_load_ui_config'); const { useHealthCheck } = jest.requireMock('../../common/hooks/use_health_check'); const { useResolveRule } = jest.requireMock('../../common/hooks/use_resolve_rule'); @@ -60,6 +64,9 @@ const { useLoadRuleTypeAadTemplateField } = jest.requireMock( ); const { useLoadRuleTypesQuery } = jest.requireMock('../../common/hooks/use_load_rule_types_query'); const { getAvailableRuleTypes } = jest.requireMock('../utils/get_authorized_rule_types'); +const { useFetchFlappingSettings } = jest.requireMock( + '../../common/hooks/use_fetch_flapping_settings' +); const uiConfigMock = { isUsingSecurity: true, @@ -103,6 +110,15 @@ useResolveRule.mockReturnValue({ data: ruleMock, }); +useFetchFlappingSettings.mockReturnValue({ + isLoading: false, + isInitialLoading: false, + data: { + lookBackWindow: 20, + statusChangeThreshold: 20, + }, +}); + const indexThresholdRuleType = { enabledInLicense: true, recoveryActionGroup: { @@ -260,6 +276,10 @@ describe('useLoadDependencies', () => { uiConfig: uiConfigMock, healthCheckError: null, fetchedFormData: ruleMock, + flappingSettings: { + lookBackWindow: 20, + statusChangeThreshold: 20, + }, connectors: [mockConnector], connectorTypes: [mockConnectorType], aadTemplateFields: [mockAadTemplateField], diff --git a/packages/kbn-alerts-ui-shared/src/rule_form/hooks/use_load_dependencies.ts b/packages/kbn-alerts-ui-shared/src/rule_form/hooks/use_load_dependencies.ts index da59e85a933a1..5e0c52b1089ba 100644 --- a/packages/kbn-alerts-ui-shared/src/rule_form/hooks/use_load_dependencies.ts +++ b/packages/kbn-alerts-ui-shared/src/rule_form/hooks/use_load_dependencies.ts @@ -22,6 +22,8 @@ import { } from '../../common/hooks'; import { getAvailableRuleTypes } from '../utils'; import { RuleTypeRegistryContract } from '../../common'; +import { useFetchFlappingSettings } from '../../common/hooks/use_fetch_flapping_settings'; +import { IS_RULE_SPECIFIC_FLAPPING_ENABLED } from '../../common/constants/rule_flapping'; import { useLoadRuleTypeAadTemplateField } from '../../common/hooks/use_load_rule_type_aad_template_fields'; export interface UseLoadDependencies { @@ -81,6 +83,15 @@ export const useLoadDependencies = (props: UseLoadDependencies) => { filteredRuleTypes, }); + const { + data: flappingSettings, + isLoading: isLoadingFlappingSettings, + isInitialLoading: isInitialLoadingFlappingSettings, + } = useFetchFlappingSettings({ + http, + enabled: IS_RULE_SPECIFIC_FLAPPING_ENABLED, + }); + const { data: connectors = [], isLoading: isLoadingConnectors, @@ -144,6 +155,7 @@ export const useLoadDependencies = (props: UseLoadDependencies) => { isLoadingUiConfig || isLoadingHealthCheck || isLoadingRuleTypes || + isLoadingFlappingSettings || isLoadingConnectors || isLoadingConnectorTypes || isLoadingAadtemplateFields @@ -156,6 +168,7 @@ export const useLoadDependencies = (props: UseLoadDependencies) => { isLoadingHealthCheck || isLoadingRule || isLoadingRuleTypes || + isLoadingFlappingSettings || isLoadingConnectors || isLoadingConnectorTypes || isLoadingAadtemplateFields @@ -166,6 +179,7 @@ export const useLoadDependencies = (props: UseLoadDependencies) => { isLoadingHealthCheck, isLoadingRule, isLoadingRuleTypes, + isLoadingFlappingSettings, isLoadingConnectors, isLoadingConnectorTypes, isLoadingAadtemplateFields, @@ -178,6 +192,7 @@ export const useLoadDependencies = (props: UseLoadDependencies) => { isInitialLoadingUiConfig || isInitialLoadingHealthCheck || isInitialLoadingRuleTypes || + isInitialLoadingFlappingSettings || isInitialLoadingConnectors || isInitialLoadingConnectorTypes || isInitialLoadingAadTemplateField @@ -190,6 +205,7 @@ export const useLoadDependencies = (props: UseLoadDependencies) => { isInitialLoadingHealthCheck || isInitialLoadingRule || isInitialLoadingRuleTypes || + isInitialLoadingFlappingSettings || isInitialLoadingConnectors || isInitialLoadingConnectorTypes || isInitialLoadingAadTemplateField @@ -200,6 +216,7 @@ export const useLoadDependencies = (props: UseLoadDependencies) => { isInitialLoadingHealthCheck, isInitialLoadingRule, isInitialLoadingRuleTypes, + isInitialLoadingFlappingSettings, isInitialLoadingConnectors, isInitialLoadingConnectorTypes, isInitialLoadingAadTemplateField, @@ -213,6 +230,7 @@ export const useLoadDependencies = (props: UseLoadDependencies) => { uiConfig, healthCheckError, fetchedFormData, + flappingSettings, connectors, connectorTypes, aadTemplateFields, diff --git a/packages/kbn-alerts-ui-shared/src/rule_form/rule_definition/rule_definition.test.tsx b/packages/kbn-alerts-ui-shared/src/rule_form/rule_definition/rule_definition.test.tsx index 01f9f39e9d086..b91148c220844 100644 --- a/packages/kbn-alerts-ui-shared/src/rule_form/rule_definition/rule_definition.test.tsx +++ b/packages/kbn-alerts-ui-shared/src/rule_form/rule_definition/rule_definition.test.tsx @@ -19,12 +19,37 @@ import type { DocLinksStart } from '@kbn/core-doc-links-browser'; import { RuleDefinition } from './rule_definition'; import { RuleType } from '@kbn/alerting-types'; import { RuleTypeModel } from '../../common/types'; +import { RuleSettingsFlappingFormProps } from '../../rule_settings/rule_settings_flapping_form'; +import { ALERT_FLAPPING_DETECTION_TITLE } from '../translations'; +import userEvent from '@testing-library/user-event'; +import { __IntlProvider as IntlProvider } from '@kbn/i18n-react'; jest.mock('../hooks', () => ({ useRuleFormState: jest.fn(), useRuleFormDispatch: jest.fn(), })); +jest.mock('../../common/constants/rule_flapping', () => ({ + IS_RULE_SPECIFIC_FLAPPING_ENABLED: true, +})); + +jest.mock('../../rule_settings/rule_settings_flapping_form', () => ({ + RuleSettingsFlappingForm: (props: RuleSettingsFlappingFormProps) => ( +
+ +
+ ), +})); + const ruleType = { id: '.es-query', name: 'Test', @@ -73,6 +98,13 @@ const plugins = { dataViews: {} as DataViewsPublicPluginStart, unifiedSearch: {} as UnifiedSearchPublicPluginStart, docLinks: {} as DocLinksStart, + application: { + capabilities: { + rulesSettings: { + writeFlappingSettingsUI: true, + }, + }, + }, }; const { useRuleFormState, useRuleFormDispatch } = jest.requireMock('../hooks'); @@ -279,4 +311,105 @@ describe('Rule Definition', () => { }, }); }); + + test('should render rule flapping settings correctly', () => { + useRuleFormState.mockReturnValue({ + plugins, + formData: { + id: 'test-id', + params: {}, + schedule: { + interval: '1m', + }, + alertDelay: { + active: 5, + }, + notifyWhen: null, + consumer: 'stackAlerts', + }, + selectedRuleType: ruleType, + selectedRuleTypeModel: ruleModel, + canShowConsumerSelection: true, + validConsumers: ['logs', 'stackAlerts'], + }); + + render(); + + expect(screen.getByText(ALERT_FLAPPING_DETECTION_TITLE)).toBeInTheDocument(); + expect(screen.getByTestId('ruleSettingsFlappingForm')).toBeInTheDocument(); + }); + + test('should allow flapping to be changed', async () => { + useRuleFormState.mockReturnValue({ + plugins, + formData: { + id: 'test-id', + params: {}, + schedule: { + interval: '1m', + }, + alertDelay: { + active: 5, + }, + notifyWhen: null, + consumer: 'stackAlerts', + }, + selectedRuleType: ruleType, + selectedRuleTypeModel: ruleModel, + canShowConsumerSelection: true, + validConsumers: ['logs', 'stackAlerts'], + }); + + render(); + + await userEvent.click(screen.getByText('onFlappingChange')); + expect(mockOnChange).toHaveBeenCalledWith({ + payload: { + property: 'flapping', + value: { + lookBackWindow: 15, + statusChangeThreshold: 15, + }, + }, + type: 'setRuleProperty', + }); + }); + + test('should open and close flapping popover when button icon is clicked', async () => { + useRuleFormState.mockReturnValue({ + plugins, + formData: { + id: 'test-id', + params: {}, + schedule: { + interval: '1m', + }, + alertDelay: { + active: 5, + }, + notifyWhen: null, + consumer: 'stackAlerts', + }, + selectedRuleType: ruleType, + selectedRuleTypeModel: ruleModel, + canShowConsumerSelection: true, + validConsumers: ['logs', 'stackAlerts'], + }); + + render( + + + + ); + + expect(screen.queryByTestId('ruleSettingsFlappingTooltipTitle')).not.toBeInTheDocument(); + + await userEvent.click(screen.getByTestId('ruleSettingsFlappingTitleTooltipButton')); + + expect(screen.queryByTestId('ruleSettingsFlappingTooltipTitle')).toBeInTheDocument(); + + await userEvent.click(screen.getByTestId('ruleSettingsFlappingTitleTooltipButton')); + + expect(screen.queryByTestId('ruleSettingsFlappingTooltipTitle')).not.toBeVisible(); + }); }); diff --git a/packages/kbn-alerts-ui-shared/src/rule_form/rule_definition/rule_definition.tsx b/packages/kbn-alerts-ui-shared/src/rule_form/rule_definition/rule_definition.tsx index fe4812436144a..3b404edc5d029 100644 --- a/packages/kbn-alerts-ui-shared/src/rule_form/rule_definition/rule_definition.tsx +++ b/packages/kbn-alerts-ui-shared/src/rule_form/rule_definition/rule_definition.tsx @@ -25,6 +25,7 @@ import { useEuiTheme, COLOR_MODES_STANDARD, } from '@elastic/eui'; +import { RuleSpecificFlappingProperties } from '@kbn/alerting-types'; import { EuiThemeProvider } from '@kbn/kibana-react-plugin/common'; import { AlertConsumers } from '@kbn/rule-data-utils'; import { @@ -39,6 +40,8 @@ import { ADVANCED_OPTIONS_TITLE, ALERT_DELAY_DESCRIPTION_TEXT, ALERT_DELAY_HELP_TEXT, + ALERT_FLAPPING_DETECTION_TITLE, + ALERT_FLAPPING_DETECTION_DESCRIPTION, } from '../translations'; import { RuleAlertDelay } from './rule_alert_delay'; import { RuleConsumerSelection } from './rule_consumer_selection'; @@ -46,6 +49,9 @@ import { RuleSchedule } from './rule_schedule'; import { useRuleFormState, useRuleFormDispatch } from '../hooks'; import { MULTI_CONSUMER_RULE_TYPE_IDS } from '../constants'; import { getAuthorizedConsumers } from '../utils'; +import { RuleSettingsFlappingTitleTooltip } from '../../rule_settings/rule_settings_flapping_title_tooltip'; +import { RuleSettingsFlappingForm } from '../../rule_settings/rule_settings_flapping_form'; +import { IS_RULE_SPECIFIC_FLAPPING_ENABLED } from '../../common/constants/rule_flapping'; export const RuleDefinition = () => { const { @@ -58,17 +64,26 @@ export const RuleDefinition = () => { selectedRuleTypeModel, validConsumers, canShowConsumerSelection = false, + flappingSettings, } = useRuleFormState(); const { colorMode } = useEuiTheme(); const dispatch = useRuleFormDispatch(); - const { charts, data, dataViews, unifiedSearch, docLinks } = plugins; + const { charts, data, dataViews, unifiedSearch, docLinks, application } = plugins; - const { params, schedule, notifyWhen } = formData; + const { + capabilities: { rulesSettings }, + } = application; + + const { writeFlappingSettingsUI } = rulesSettings || {}; + + const { params, schedule, notifyWhen, flapping } = formData; const [isAdvancedOptionsVisible, setIsAdvancedOptionsVisible] = useState(false); + const [isFlappingPopoverOpen, setIsFlappingPopoverOpen] = useState(false); + const authorizedConsumers = useMemo(() => { if (!validConsumers?.length) { return []; @@ -143,6 +158,19 @@ export const RuleDefinition = () => { [dispatch] ); + const onSetFlapping = useCallback( + (value: RuleSpecificFlappingProperties | null) => { + dispatch({ + type: 'setRuleProperty', + payload: { + property: 'flapping', + value, + }, + }); + }, + [dispatch] + ); + return ( @@ -243,7 +271,10 @@ export const RuleDefinition = () => { { + setIsAdvancedOptionsVisible(isOpen); + setIsFlappingPopoverOpen(false); + }} initialIsOpen={isAdvancedOptionsVisible} buttonProps={{ 'data-test-subj': 'advancedOptionsAccordionButton', @@ -274,6 +305,31 @@ export const RuleDefinition = () => { > + {IS_RULE_SPECIFIC_FLAPPING_ENABLED && ( + {ALERT_FLAPPING_DETECTION_TITLE}} + description={ + +

+ {ALERT_FLAPPING_DETECTION_DESCRIPTION} + +

+
+ } + > + +
+ )}
diff --git a/packages/kbn-alerts-ui-shared/src/rule_form/translations.ts b/packages/kbn-alerts-ui-shared/src/rule_form/translations.ts index e7b060dce9831..20e87c66f10f4 100644 --- a/packages/kbn-alerts-ui-shared/src/rule_form/translations.ts +++ b/packages/kbn-alerts-ui-shared/src/rule_form/translations.ts @@ -85,6 +85,21 @@ export const ALERT_DELAY_TITLE_PREFIX = i18n.translate( } ); +export const ALERT_FLAPPING_DETECTION_TITLE = i18n.translate( + 'alertsUIShared.ruleForm.ruleDefinition.alertFlappingDetectionTitle', + { + defaultMessage: 'Alert flapping detection', + } +); + +export const ALERT_FLAPPING_DETECTION_DESCRIPTION = i18n.translate( + 'alertsUIShared.ruleForm.ruleDefinition.alertFlappingDetectionDescription', + { + defaultMessage: + 'Detect alerts that switch quickly between active and recovered states and reduce unwanted noise for these flapping alerts', + } +); + export const SCHEDULE_TITLE_PREFIX = i18n.translate( 'alertsUIShared.ruleForm.ruleSchedule.scheduleTitlePrefix', { diff --git a/packages/kbn-alerts-ui-shared/src/rule_form/types.ts b/packages/kbn-alerts-ui-shared/src/rule_form/types.ts index ac81f45de19e6..d33c74da528db 100644 --- a/packages/kbn-alerts-ui-shared/src/rule_form/types.ts +++ b/packages/kbn-alerts-ui-shared/src/rule_form/types.ts @@ -20,7 +20,7 @@ import type { UnifiedSearchPublicPluginStart } from '@kbn/unified-search-plugin/ import type { SettingsStart } from '@kbn/core-ui-settings-browser'; import { RuleCreationValidConsumer } from '@kbn/rule-data-utils'; import { ActionType } from '@kbn/actions-types'; -import { ActionVariable } from '@kbn/alerting-types'; +import { ActionVariable, RulesSettingsFlapping } from '@kbn/alerting-types'; import { ActionConnector, ActionTypeRegistryContract, @@ -46,6 +46,7 @@ export interface RuleFormData { alertDelay?: Rule['alertDelay']; notifyWhen?: Rule['notifyWhen']; ruleTypeId?: Rule['ruleTypeId']; + flapping?: Rule['flapping']; } export interface RuleFormPlugins { @@ -83,6 +84,7 @@ export interface RuleFormState { minimumScheduleInterval?: MinimumScheduleInterval; canShowConsumerSelection?: boolean; validConsumers?: RuleCreationValidConsumer[]; + flappingSettings?: RulesSettingsFlapping; } export type InitialRule = Partial & diff --git a/packages/kbn-alerts-ui-shared/src/rule_settings/rule_settings_flapping_form.tsx b/packages/kbn-alerts-ui-shared/src/rule_settings/rule_settings_flapping_form.tsx new file mode 100644 index 0000000000000..99f64f0a3977f --- /dev/null +++ b/packages/kbn-alerts-ui-shared/src/rule_settings/rule_settings_flapping_form.tsx @@ -0,0 +1,318 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the "Elastic License + * 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side + * Public License v 1"; you may not use this file except in compliance with, at + * your election, the "Elastic License 2.0", the "GNU Affero General Public + * License v3.0 only", or the "Server Side Public License, v 1". + */ + +import React, { useCallback, useMemo, useRef, useState } from 'react'; +import { + EuiBadge, + EuiButtonIcon, + EuiFlexGroup, + EuiFlexItem, + EuiHorizontalRule, + EuiLink, + EuiPopover, + EuiSpacer, + EuiSplitPanel, + EuiSwitch, + EuiText, + EuiOutsideClickDetector, + useEuiTheme, + useIsWithinMinBreakpoint, +} from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; +import { RuleSpecificFlappingProperties, RulesSettingsFlapping } from '@kbn/alerting-types'; +import { FormattedMessage } from '@kbn/i18n-react'; +import { RuleSettingsFlappingMessage } from './rule_settings_flapping_message'; +import { RuleSettingsFlappingInputs } from './rule_settings_flapping_inputs'; + +const flappingLabel = i18n.translate('alertsUIShared.ruleSettingsFlappingForm.flappingLabel', { + defaultMessage: 'Flapping Detection', +}); + +const flappingOnLabel = i18n.translate('alertsUIShared.ruleSettingsFlappingForm.onLabel', { + defaultMessage: 'ON', +}); + +const flappingOffLabel = i18n.translate('alertsUIShared.ruleSettingsFlappingForm.offLabel', { + defaultMessage: 'OFF', +}); + +const flappingOverrideLabel = i18n.translate( + 'alertsUIShared.ruleSettingsFlappingForm.overrideLabel', + { + defaultMessage: 'Custom', + } +); + +const flappingOffContentRules = i18n.translate( + 'alertsUIShared.ruleSettingsFlappingForm.flappingOffContentRules', + { + defaultMessage: 'Rules', + } +); + +const flappingOffContentSettings = i18n.translate( + 'alertsUIShared.ruleSettingsFlappingForm.flappingOffContentSettings', + { + defaultMessage: 'Settings', + } +); + +const flappingExternalLinkLabel = i18n.translate( + 'alertsUIShared.ruleSettingsFlappingForm.flappingExternalLinkLabel', + { + defaultMessage: "What's this?", + } +); + +const flappingOverrideConfiguration = i18n.translate( + 'alertsUIShared.ruleSettingsFlappingForm.flappingOverrideConfiguration', + { + defaultMessage: 'Customize Configuration', + } +); + +const clampFlappingValues = (flapping: RuleSpecificFlappingProperties) => { + return { + ...flapping, + statusChangeThreshold: Math.min(flapping.lookBackWindow, flapping.statusChangeThreshold), + }; +}; + +export interface RuleSettingsFlappingFormProps { + flappingSettings?: RuleSpecificFlappingProperties | null; + spaceFlappingSettings?: RulesSettingsFlapping; + canWriteFlappingSettingsUI: boolean; + onFlappingChange: (value: RuleSpecificFlappingProperties | null) => void; +} + +export const RuleSettingsFlappingForm = (props: RuleSettingsFlappingFormProps) => { + const { flappingSettings, spaceFlappingSettings, canWriteFlappingSettingsUI, onFlappingChange } = + props; + + const [isPopoverOpen, setIsPopoverOpen] = useState(false); + + const cachedFlappingSettings = useRef(); + + const isDesktop = useIsWithinMinBreakpoint('xl'); + + const { euiTheme } = useEuiTheme(); + + const onFlappingToggle = useCallback(() => { + if (!spaceFlappingSettings) { + return; + } + if (flappingSettings) { + cachedFlappingSettings.current = flappingSettings; + return onFlappingChange(null); + } + const initialFlappingSettings = cachedFlappingSettings.current || spaceFlappingSettings; + onFlappingChange({ + lookBackWindow: initialFlappingSettings.lookBackWindow, + statusChangeThreshold: initialFlappingSettings.statusChangeThreshold, + }); + }, [spaceFlappingSettings, flappingSettings, onFlappingChange]); + + const internalOnFlappingChange = useCallback( + (flapping: RuleSpecificFlappingProperties) => { + const clampedValue = clampFlappingValues(flapping); + onFlappingChange(clampedValue); + cachedFlappingSettings.current = clampedValue; + }, + [onFlappingChange] + ); + + const onLookBackWindowChange = useCallback( + (value: number) => { + if (!flappingSettings) { + return; + } + internalOnFlappingChange({ + ...flappingSettings, + lookBackWindow: value, + }); + }, + [flappingSettings, internalOnFlappingChange] + ); + + const onStatusChangeThresholdChange = useCallback( + (value: number) => { + if (!flappingSettings) { + return; + } + internalOnFlappingChange({ + ...flappingSettings, + statusChangeThreshold: value, + }); + }, + [flappingSettings, internalOnFlappingChange] + ); + + const flappingOffTooltip = useMemo(() => { + if (!spaceFlappingSettings) { + return null; + } + const { enabled } = spaceFlappingSettings; + if (enabled) { + return null; + } + + if (canWriteFlappingSettingsUI) { + return ( + setIsPopoverOpen(false)}> + setIsPopoverOpen(!isPopoverOpen)} + /> + } + > + + {flappingOffContentRules}, + settings: {flappingOffContentSettings}, + }} + /> + + + + ); + } + // TODO: Add the external doc link here! + return ( + + {flappingExternalLinkLabel} + + ); + }, [canWriteFlappingSettingsUI, isPopoverOpen, spaceFlappingSettings]); + + const flappingFormHeader = useMemo(() => { + if (!spaceFlappingSettings) { + return null; + } + const { enabled } = spaceFlappingSettings; + + return ( + + + + + {flappingLabel} + + + {enabled ? flappingOnLabel : flappingOffLabel} + + {flappingSettings && enabled && ( + {flappingOverrideLabel} + )} + + + {enabled && ( + + )} + {flappingOffTooltip} + + + {flappingSettings && enabled && ( + <> + + + + )} + + ); + }, [ + isDesktop, + euiTheme, + spaceFlappingSettings, + flappingSettings, + flappingOffTooltip, + onFlappingToggle, + ]); + + const flappingFormBody = useMemo(() => { + if (!flappingSettings) { + return null; + } + if (!spaceFlappingSettings?.enabled) { + return null; + } + return ( + + + + ); + }, [ + flappingSettings, + spaceFlappingSettings, + onLookBackWindowChange, + onStatusChangeThresholdChange, + ]); + + const flappingFormMessage = useMemo(() => { + if (!spaceFlappingSettings || !spaceFlappingSettings.enabled) { + return null; + } + const settingsToUse = flappingSettings || spaceFlappingSettings; + return ( + + + + ); + }, [spaceFlappingSettings, flappingSettings, euiTheme]); + + return ( + + + + {flappingFormHeader} + {flappingFormBody} + + + {flappingFormMessage} + + ); +}; diff --git a/packages/kbn-alerts-ui-shared/src/rule_settings/rule_settings_flapping_message.tsx b/packages/kbn-alerts-ui-shared/src/rule_settings/rule_settings_flapping_message.tsx index b7c8681ef221b..d6d488e08f0c1 100644 --- a/packages/kbn-alerts-ui-shared/src/rule_settings/rule_settings_flapping_message.tsx +++ b/packages/kbn-alerts-ui-shared/src/rule_settings/rule_settings_flapping_message.tsx @@ -37,21 +37,34 @@ export const flappingOffMessage = i18n.translate( export interface RuleSettingsFlappingMessageProps { lookBackWindow: number; statusChangeThreshold: number; + isUsingRuleSpecificFlapping: boolean; } export const RuleSettingsFlappingMessage = (props: RuleSettingsFlappingMessageProps) => { - const { lookBackWindow, statusChangeThreshold } = props; + const { lookBackWindow, statusChangeThreshold, isUsingRuleSpecificFlapping } = props; return ( - {getLookBackWindowLabelRuleRuns(lookBackWindow)}, - statusChangeThreshold: {getStatusChangeThresholdRuleRuns(statusChangeThreshold)}, - }} - /> + {!isUsingRuleSpecificFlapping && ( + {getLookBackWindowLabelRuleRuns(lookBackWindow)}, + statusChangeThreshold: {getStatusChangeThresholdRuleRuns(statusChangeThreshold)}, + }} + /> + )} + {isUsingRuleSpecificFlapping && ( + {getLookBackWindowLabelRuleRuns(lookBackWindow)}, + statusChangeThreshold: {getStatusChangeThresholdRuleRuns(statusChangeThreshold)}, + }} + /> + )} ); }; diff --git a/packages/kbn-alerts-ui-shared/src/rule_settings/rule_settings_flapping_title_tooltip.tsx b/packages/kbn-alerts-ui-shared/src/rule_settings/rule_settings_flapping_title_tooltip.tsx new file mode 100644 index 0000000000000..2a5cc4186013d --- /dev/null +++ b/packages/kbn-alerts-ui-shared/src/rule_settings/rule_settings_flapping_title_tooltip.tsx @@ -0,0 +1,140 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the "Elastic License + * 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side + * Public License v 1"; you may not use this file except in compliance with, at + * your election, the "Elastic License 2.0", the "GNU Affero General Public + * License v3.0 only", or the "Server Side Public License, v 1". + */ + +import { + EuiButtonIcon, + EuiPopover, + EuiPopoverProps, + EuiPopoverTitle, + EuiSpacer, + EuiText, + EuiOutsideClickDetector, +} from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; +import React from 'react'; +import { FormattedMessage } from '@kbn/i18n-react'; + +const tooltipTitle = i18n.translate( + 'alertsUIShared.ruleSettingsFlappingTitleTooltip.tooltipTitle', + { + defaultMessage: 'Alert flapping detection', + } +); + +const flappingTitlePopoverFlappingDetection = i18n.translate( + 'alertsUIShared.ruleSettingsFlappingTitleTooltip.flappingTitlePopoverFlappingDetection', + { + defaultMessage: 'flapping detection', + } +); + +const flappingTitlePopoverAlertStatus = i18n.translate( + 'alertsUIShared.ruleSettingsFlappingTitleTooltip.flappingTitlePopoverAlertStatus', + { + defaultMessage: 'alert status change threshold', + } +); + +const flappingTitlePopoverLookBack = i18n.translate( + 'alertsUIShared.ruleSettingsFlappingTitleTooltip.flappingTitlePopoverLookBack', + { + defaultMessage: 'rule run look back window', + } +); + +const flappingOffContentRules = i18n.translate( + 'alertsUIShared.ruleSettingsFlappingTitleTooltip.flappingOffContentRules', + { + defaultMessage: 'Rules', + } +); + +const flappingOffContentSettings = i18n.translate( + 'alertsUIShared.ruleSettingsFlappingTitleTooltip.flappingOffContentSettings', + { + defaultMessage: 'Settings', + } +); + +interface RuleSettingsFlappingTitleTooltipProps { + isOpen: boolean; + setIsPopoverOpen: (isOpen: boolean) => void; + anchorPosition?: EuiPopoverProps['anchorPosition']; +} + +export const RuleSettingsFlappingTitleTooltip = (props: RuleSettingsFlappingTitleTooltipProps) => { + const { isOpen, setIsPopoverOpen, anchorPosition = 'leftCenter' } = props; + + return ( + setIsPopoverOpen(false)}> + setIsPopoverOpen(!isOpen)} + /> + } + > + + {tooltipTitle} + + + {flappingTitlePopoverFlappingDetection}, + }} + /> + + + + {flappingTitlePopoverAlertStatus}, + }} + /> + + + + {flappingTitlePopoverLookBack}, + }} + /> + + + + {flappingOffContentRules}, + settings: {flappingOffContentSettings}, + }} + /> + + + + ); +}; diff --git a/packages/kbn-alerts-ui-shared/src/rule_type_modal/components/rule_type_list.tsx b/packages/kbn-alerts-ui-shared/src/rule_type_modal/components/rule_type_list.tsx index 9b84bb83cf1fa..4e21e428f3c5c 100644 --- a/packages/kbn-alerts-ui-shared/src/rule_type_modal/components/rule_type_list.tsx +++ b/packages/kbn-alerts-ui-shared/src/rule_type_modal/components/rule_type_list.tsx @@ -97,6 +97,8 @@ export const RuleTypeList: React.FC = ({ grow={1} style={{ paddingTop: euiTheme.size.base /* Match drop shadow padding in the right column */, + paddingRight: euiTheme.size.base, + overflowY: 'auto', }} > diff --git a/packages/kbn-apm-synthtrace-client/src/lib/entity.ts b/packages/kbn-apm-synthtrace-client/src/lib/entity.ts index 4d522ef07ff0e..b26dbfc7ffb46 100644 --- a/packages/kbn-apm-synthtrace-client/src/lib/entity.ts +++ b/packages/kbn-apm-synthtrace-client/src/lib/entity.ts @@ -7,6 +7,8 @@ * License v3.0 only", or the "Server Side Public License, v 1". */ +export type ObjectEntry = [keyof T, T[keyof T]]; + export type Fields | undefined = undefined> = { '@timestamp'?: number; } & (TMeta extends undefined ? {} : Partial<{ meta: TMeta }>); @@ -27,4 +29,14 @@ export class Entity { return this; } + + overrides(overrides: Partial) { + const overrideEntries = Object.entries(overrides) as Array>; + + overrideEntries.forEach(([fieldName, value]) => { + this.fields[fieldName] = value; + }); + + return this; + } } diff --git a/packages/kbn-apm-synthtrace-client/src/lib/gaussian_events.ts b/packages/kbn-apm-synthtrace-client/src/lib/gaussian_events.ts new file mode 100644 index 0000000000000..4f1db28017d29 --- /dev/null +++ b/packages/kbn-apm-synthtrace-client/src/lib/gaussian_events.ts @@ -0,0 +1,74 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the "Elastic License + * 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side + * Public License v 1"; you may not use this file except in compliance with, at + * your election, the "Elastic License 2.0", the "GNU Affero General Public + * License v3.0 only", or the "Server Side Public License, v 1". + */ + +import { castArray } from 'lodash'; +import { SynthtraceGenerator } from '../types'; +import { Fields } from './entity'; +import { Serializable } from './serializable'; + +export class GaussianEvents { + constructor( + private readonly from: Date, + private readonly to: Date, + private readonly mean: Date, + private readonly width: number, + private readonly totalPoints: number + ) {} + + *generator( + map: ( + timestamp: number, + index: number + ) => Serializable | Array> + ): SynthtraceGenerator { + if (this.totalPoints <= 0) { + return; + } + + const startTime = this.from.getTime(); + const endTime = this.to.getTime(); + const meanTime = this.mean.getTime(); + const densityInterval = 1 / (this.totalPoints - 1); + + for (let eventIndex = 0; eventIndex < this.totalPoints; eventIndex++) { + const quantile = eventIndex * densityInterval; + + const standardScore = Math.sqrt(2) * inverseError(2 * quantile - 1); + const timestamp = Math.round(meanTime + standardScore * this.width); + + if (timestamp >= startTime && timestamp <= endTime) { + yield* this.generateEvents(timestamp, eventIndex, map); + } + } + } + + private *generateEvents( + timestamp: number, + eventIndex: number, + map: ( + timestamp: number, + index: number + ) => Serializable | Array> + ): Generator> { + const events = castArray(map(timestamp, eventIndex)); + for (const event of events) { + yield event; + } + } +} + +function inverseError(x: number): number { + const a = 0.147; + const sign = x < 0 ? -1 : 1; + + const part1 = 2 / (Math.PI * a) + Math.log(1 - x * x) / 2; + const part2 = Math.log(1 - x * x) / a; + + return sign * Math.sqrt(Math.sqrt(part1 * part1 - part2) - part1); +} diff --git a/packages/kbn-apm-synthtrace-client/src/lib/infra/host.ts b/packages/kbn-apm-synthtrace-client/src/lib/infra/host.ts index 198949b482be3..30550d64c4df8 100644 --- a/packages/kbn-apm-synthtrace-client/src/lib/infra/host.ts +++ b/packages/kbn-apm-synthtrace-client/src/lib/infra/host.ts @@ -27,7 +27,7 @@ interface HostDocument extends Fields { 'cloud.provider'?: string; } -class Host extends Entity { +export class Host extends Entity { cpu({ cpuTotalValue }: { cpuTotalValue?: number } = {}) { return new HostMetrics({ ...this.fields, @@ -175,3 +175,11 @@ export function host(name: string): Host { 'cloud.provider': 'gcp', }); } + +export function minimalHost(name: string): Host { + return new Host({ + 'agent.id': 'synthtrace', + 'host.hostname': name, + 'host.name': name, + }); +} diff --git a/packages/kbn-apm-synthtrace-client/src/lib/infra/index.ts b/packages/kbn-apm-synthtrace-client/src/lib/infra/index.ts index 853a9549ce02c..2957605cffcd3 100644 --- a/packages/kbn-apm-synthtrace-client/src/lib/infra/index.ts +++ b/packages/kbn-apm-synthtrace-client/src/lib/infra/index.ts @@ -8,7 +8,7 @@ */ import { dockerContainer, DockerContainerMetricsDocument } from './docker_container'; -import { host, HostMetricsDocument } from './host'; +import { host, HostMetricsDocument, minimalHost } from './host'; import { k8sContainer, K8sContainerMetricsDocument } from './k8s_container'; import { pod, PodMetricsDocument } from './pod'; import { awsRds, AWSRdsMetricsDocument } from './aws/rds'; @@ -24,6 +24,7 @@ export type InfraDocument = export const infra = { host, + minimalHost, pod, dockerContainer, k8sContainer, diff --git a/packages/kbn-apm-synthtrace-client/src/lib/interval.ts b/packages/kbn-apm-synthtrace-client/src/lib/interval.ts index 1d56c42e1fe12..5a5ed3ab5fdbe 100644 --- a/packages/kbn-apm-synthtrace-client/src/lib/interval.ts +++ b/packages/kbn-apm-synthtrace-client/src/lib/interval.ts @@ -34,6 +34,10 @@ interface IntervalOptions { rate?: number; } +interface StepDetails { + stepMilliseconds: number; +} + export class Interval { private readonly intervalAmount: number; private readonly intervalUnit: unitOfTime.DurationConstructor; @@ -46,12 +50,16 @@ export class Interval { this._rate = options.rate || 1; } + private getIntervalMilliseconds(): number { + return moment.duration(this.intervalAmount, this.intervalUnit).asMilliseconds(); + } + private getTimestamps() { const from = this.options.from.getTime(); const to = this.options.to.getTime(); let time: number = from; - const diff = moment.duration(this.intervalAmount, this.intervalUnit).asMilliseconds(); + const diff = this.getIntervalMilliseconds(); const timestamps: number[] = []; @@ -68,15 +76,19 @@ export class Interval { *generator( map: ( timestamp: number, - index: number + index: number, + stepDetails: StepDetails ) => Serializable | Array> ): SynthtraceGenerator { const timestamps = this.getTimestamps(); + const stepDetails: StepDetails = { + stepMilliseconds: this.getIntervalMilliseconds(), + }; let index = 0; for (const timestamp of timestamps) { - const events = castArray(map(timestamp, index)); + const events = castArray(map(timestamp, index, stepDetails)); index++; for (const event of events) { yield event; diff --git a/packages/kbn-apm-synthtrace-client/src/lib/logs/index.ts b/packages/kbn-apm-synthtrace-client/src/lib/logs/index.ts index e19f0f6fd6565..2bbc59eb37e70 100644 --- a/packages/kbn-apm-synthtrace-client/src/lib/logs/index.ts +++ b/packages/kbn-apm-synthtrace-client/src/lib/logs/index.ts @@ -68,6 +68,7 @@ export type LogDocument = Fields & 'event.duration': number; 'event.start': Date; 'event.end': Date; + labels?: Record; test_field: string | string[]; date: Date; severity: string; @@ -156,6 +157,26 @@ function create(logsOptions: LogsOptions = defaultLogsOptions): Log { ).dataset('synth'); } +function createMinimal({ + dataset = 'synth', + namespace = 'default', +}: { + dataset?: string; + namespace?: string; +} = {}): Log { + return new Log( + { + 'input.type': 'logs', + 'data_stream.namespace': namespace, + 'data_stream.type': 'logs', + 'data_stream.dataset': dataset, + 'event.dataset': dataset, + }, + { isLogsDb: false } + ); +} + export const log = { create, + createMinimal, }; diff --git a/packages/kbn-apm-synthtrace-client/src/lib/poisson_events.test.ts b/packages/kbn-apm-synthtrace-client/src/lib/poisson_events.test.ts new file mode 100644 index 0000000000000..0741884550f32 --- /dev/null +++ b/packages/kbn-apm-synthtrace-client/src/lib/poisson_events.test.ts @@ -0,0 +1,53 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the "Elastic License + * 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side + * Public License v 1"; you may not use this file except in compliance with, at + * your election, the "Elastic License 2.0", the "GNU Affero General Public + * License v3.0 only", or the "Server Side Public License, v 1". + */ + +import { PoissonEvents } from './poisson_events'; +import { Serializable } from './serializable'; + +describe('poisson events', () => { + it('generates events within the given time range', () => { + const poissonEvents = new PoissonEvents(new Date(1000), new Date(2000), 10); + + const events = Array.from( + poissonEvents.generator((timestamp) => new Serializable({ '@timestamp': timestamp })) + ); + + expect(events.length).toBeGreaterThanOrEqual(1); + + for (const event of events) { + expect(event.fields['@timestamp']).toBeGreaterThanOrEqual(1000); + expect(event.fields['@timestamp']).toBeLessThanOrEqual(2000); + } + }); + + it('generates at least one event if the rate is greater than 0', () => { + const poissonEvents = new PoissonEvents(new Date(1000), new Date(2000), 1); + + const events = Array.from( + poissonEvents.generator((timestamp) => new Serializable({ '@timestamp': timestamp })) + ); + + expect(events.length).toBeGreaterThanOrEqual(1); + + for (const event of events) { + expect(event.fields['@timestamp']).toBeGreaterThanOrEqual(1000); + expect(event.fields['@timestamp']).toBeLessThanOrEqual(2000); + } + }); + + it('generates no event if the rate is 0', () => { + const poissonEvents = new PoissonEvents(new Date(1000), new Date(2000), 0); + + const events = Array.from( + poissonEvents.generator((timestamp) => new Serializable({ '@timestamp': timestamp })) + ); + + expect(events.length).toBe(0); + }); +}); diff --git a/packages/kbn-apm-synthtrace-client/src/lib/poisson_events.ts b/packages/kbn-apm-synthtrace-client/src/lib/poisson_events.ts new file mode 100644 index 0000000000000..e7fd24b8323e7 --- /dev/null +++ b/packages/kbn-apm-synthtrace-client/src/lib/poisson_events.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", the "GNU Affero General Public License v3.0 only", and the "Server Side + * Public License v 1"; you may not use this file except in compliance with, at + * your election, the "Elastic License 2.0", the "GNU Affero General Public + * License v3.0 only", or the "Server Side Public License, v 1". + */ + +import { castArray } from 'lodash'; +import { SynthtraceGenerator } from '../types'; +import { Fields } from './entity'; +import { Serializable } from './serializable'; + +export class PoissonEvents { + constructor( + private readonly from: Date, + private readonly to: Date, + private readonly rate: number + ) {} + + private getTotalTimePeriod(): number { + return this.to.getTime() - this.from.getTime(); + } + + private getInterarrivalTime(): number { + const distribution = -Math.log(1 - Math.random()) / this.rate; + const totalTimePeriod = this.getTotalTimePeriod(); + return Math.floor(distribution * totalTimePeriod); + } + + *generator( + map: ( + timestamp: number, + index: number + ) => Serializable | Array> + ): SynthtraceGenerator { + if (this.rate <= 0) { + return; + } + + let currentTime = this.from.getTime(); + const endTime = this.to.getTime(); + let eventIndex = 0; + + while (currentTime < endTime) { + const interarrivalTime = this.getInterarrivalTime(); + currentTime += interarrivalTime; + + if (currentTime < endTime) { + yield* this.generateEvents(currentTime, eventIndex, map); + eventIndex++; + } + } + + // ensure at least one event has been emitted + if (this.rate > 0 && eventIndex === 0) { + const forcedEventTime = + this.from.getTime() + Math.floor(Math.random() * this.getTotalTimePeriod()); + yield* this.generateEvents(forcedEventTime, eventIndex, map); + } + } + + private *generateEvents( + timestamp: number, + eventIndex: number, + map: ( + timestamp: number, + index: number + ) => Serializable | Array> + ): Generator> { + const events = castArray(map(timestamp, eventIndex)); + for (const event of events) { + yield event; + } + } +} diff --git a/packages/kbn-apm-synthtrace-client/src/lib/timerange.ts b/packages/kbn-apm-synthtrace-client/src/lib/timerange.ts index ccdea4ee75197..1c6f12414a148 100644 --- a/packages/kbn-apm-synthtrace-client/src/lib/timerange.ts +++ b/packages/kbn-apm-synthtrace-client/src/lib/timerange.ts @@ -9,10 +9,12 @@ import datemath from '@kbn/datemath'; import type { Moment } from 'moment'; +import { GaussianEvents } from './gaussian_events'; import { Interval } from './interval'; +import { PoissonEvents } from './poisson_events'; export class Timerange { - constructor(private from: Date, private to: Date) {} + constructor(public readonly from: Date, public readonly to: Date) {} interval(interval: string) { return new Interval({ from: this.from, to: this.to, interval }); @@ -21,6 +23,29 @@ export class Timerange { ratePerMinute(rate: number) { return this.interval(`1m`).rate(rate); } + + poissonEvents(rate: number) { + return new PoissonEvents(this.from, this.to, rate); + } + + gaussianEvents(mean: Date, width: number, totalPoints: number) { + return new GaussianEvents(this.from, this.to, mean, width, totalPoints); + } + + splitInto(segmentCount: number): Timerange[] { + const duration = this.to.getTime() - this.from.getTime(); + const segmentDuration = duration / segmentCount; + + return Array.from({ length: segmentCount }, (_, i) => { + const from = new Date(this.from.getTime() + i * segmentDuration); + const to = new Date(from.getTime() + segmentDuration); + return new Timerange(from, to); + }); + } + + toString() { + return `Timerange(from=${this.from.toISOString()}, to=${this.to.toISOString()})`; + } } type DateLike = Date | number | Moment | string; diff --git a/packages/kbn-apm-synthtrace/src/lib/logs/custom_logsdb_index_templates.ts b/packages/kbn-apm-synthtrace/src/lib/logs/custom_logsdb_index_templates.ts index a0b155444919e..3eadd3f3941de 100644 --- a/packages/kbn-apm-synthtrace/src/lib/logs/custom_logsdb_index_templates.ts +++ b/packages/kbn-apm-synthtrace/src/lib/logs/custom_logsdb_index_templates.ts @@ -25,6 +25,7 @@ export const indexTemplates: { template: { settings: { mode: 'logsdb', + default_pipeline: 'logs@default-pipeline', }, }, priority: 500, diff --git a/packages/kbn-apm-synthtrace/src/lib/logs/logs_synthtrace_es_client.ts b/packages/kbn-apm-synthtrace/src/lib/logs/logs_synthtrace_es_client.ts index a6a64429f9b86..9673d1678132b 100644 --- a/packages/kbn-apm-synthtrace/src/lib/logs/logs_synthtrace_es_client.ts +++ b/packages/kbn-apm-synthtrace/src/lib/logs/logs_synthtrace_es_client.ts @@ -7,16 +7,20 @@ * License v3.0 only", or the "Server Side Public License, v 1". */ -import { Client } from '@elastic/elasticsearch'; +import { Client, estypes } from '@elastic/elasticsearch'; import { pipeline, Readable } from 'stream'; import { LogDocument } from '@kbn/apm-synthtrace-client/src/lib/logs'; -import { MappingTypeMapping } from '@elastic/elasticsearch/lib/api/types'; +import { IngestProcessorContainer, MappingTypeMapping } from '@elastic/elasticsearch/lib/api/types'; +import { ValuesType } from 'utility-types'; import { SynthtraceEsClient, SynthtraceEsClientOptions } from '../shared/base_client'; import { getSerializeTransform } from '../shared/get_serialize_transform'; import { Logger } from '../utils/create_logger'; import { indexTemplates, IndexTemplateName } from './custom_logsdb_index_templates'; import { getRoutingTransform } from '../shared/data_stream_get_routing_transform'; +export const LogsIndex = 'logs'; +export const LogsCustom = 'logs@custom'; + export type LogsSynthtraceEsClientOptions = Omit; export class LogsSynthtraceEsClient extends SynthtraceEsClient { @@ -60,6 +64,47 @@ export class LogsSynthtraceEsClient extends SynthtraceEsClient { this.logger.error(`Index creation failed: ${index} - ${err.message}`); } } + + async updateIndexTemplate( + indexName: string, + modify: ( + template: ValuesType< + estypes.IndicesGetIndexTemplateResponse['index_templates'] + >['index_template'] + ) => estypes.IndicesPutIndexTemplateRequest + ) { + try { + const response = await this.client.indices.getIndexTemplate({ + name: indexName, + }); + + await Promise.all( + response.index_templates.map((template) => { + return this.client.indices.putIndexTemplate({ + ...modify(template.index_template), + name: template.name, + }); + }) + ); + + this.logger.info(`Updated ${indexName} index template`); + } catch (err) { + this.logger.error(`Update index template failed: ${indexName} - ${err.message}`); + } + } + + async createCustomPipeline(processors: IngestProcessorContainer[]) { + try { + this.client.ingest.putPipeline({ + id: LogsCustom, + processors, + version: 1, + }); + this.logger.info(`Custom pipeline created: ${LogsCustom}`); + } catch (err) { + this.logger.error(`Custom pipeline creation failed: ${LogsCustom} - ${err.message}`); + } + } } function logsPipeline() { diff --git a/packages/kbn-apm-synthtrace/src/scenarios/degraded_logs.ts b/packages/kbn-apm-synthtrace/src/scenarios/degraded_logs.ts index 47dd4ffd2652f..b3e41bbdd4e28 100644 --- a/packages/kbn-apm-synthtrace/src/scenarios/degraded_logs.ts +++ b/packages/kbn-apm-synthtrace/src/scenarios/degraded_logs.ts @@ -16,12 +16,10 @@ import { getCluster, getCloudRegion, getCloudProvider, + MORE_THAN_1024_CHARS, } from './helpers/logs_mock_data'; import { parseLogsScenarioOpts } from './helpers/logs_scenario_opts_parser'; -const MORE_THAN_1024_CHARS = - 'Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum. Sed ut perspiciatis unde omnis iste natus error sit voluptatem accusantium doloremque laudantium, totam rem aperiam, eaque ipsa quae ab illo inventore veritatis et quasi architecto beatae vitae dicta sunt explicabo. Nemo enim ipsam voluptatem quia voluptas sit aspernatur aut odit aut fugit, sed quia consequuntur magni dolores eos qui ratione voluptatem sequi nesciunt. Neque porro quisquam est, qui dolorem ipsum quia dolor sit amet, consectetur, adipisci velit, sed quia non numquam eius modi tempora incidunt ut labore et dolore magnam aliquam quaerat voluptatem. Ut enim ad minima veniam, quis nostrum exercitationem ullam corporis suscipit laboriosam, nisi ut aliquid ex ea commodi consequatur? Quis autem vel eum iure reprehenderit qui in ea voluptate velit esse quam nihil molestiae consequatur, vel illum qui dolorem eum fugiat quo voluptas nulla pariatur?'; - // Logs Data logic const MESSAGE_LOG_LEVELS = [ { message: 'A simple log', level: 'info' }, diff --git a/packages/kbn-apm-synthtrace/src/scenarios/degraded_synthetics_monitors.ts b/packages/kbn-apm-synthtrace/src/scenarios/degraded_synthetics_monitors.ts index c61fecd8b7109..6e00bfd0abf15 100644 --- a/packages/kbn-apm-synthtrace/src/scenarios/degraded_synthetics_monitors.ts +++ b/packages/kbn-apm-synthtrace/src/scenarios/degraded_synthetics_monitors.ts @@ -14,12 +14,9 @@ import { } from '@kbn/apm-synthtrace-client'; import { Scenario } from '../cli/scenario'; import { withClient } from '../lib/utils/with_client'; -import { getIpAddress } from './helpers/logs_mock_data'; +import { MORE_THAN_1024_CHARS, getIpAddress } from './helpers/logs_mock_data'; import { getAtIndexOrRandom } from './helpers/get_at_index_or_random'; -const MORE_THAN_1024_CHARS = - 'Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum. Sed ut perspiciatis unde omnis iste natus error sit voluptatem accusantium doloremque laudantium, totam rem aperiam, eaque ipsa quae ab illo inventore veritatis et quasi architecto beatae vitae dicta sunt explicabo. Nemo enim ipsam voluptatem quia voluptas sit aspernatur aut odit aut fugit, sed quia consequuntur magni dolores eos qui ratione voluptatem sequi nesciunt. Neque porro quisquam est, qui dolorem ipsum quia dolor sit amet, consectetur, adipisci velit, sed quia non numquam eius modi tempora incidunt ut labore et dolore magnam aliquam quaerat voluptatem. Ut enim ad minima veniam, quis nostrum exercitationem ullam corporis suscipit laboriosam, nisi ut aliquid ex ea commodi consequatur? Quis autem vel eum iure reprehenderit qui in ea voluptate velit esse quam nihil molestiae consequatur, vel illum qui dolorem eum fugiat quo voluptas nulla pariatur?'; - const MONITOR_NAMES = Array(4) .fill(null) .map((_, idx) => `synth-monitor-${idx}`); diff --git a/packages/kbn-apm-synthtrace/src/scenarios/distributed_unstructured_logs.ts b/packages/kbn-apm-synthtrace/src/scenarios/distributed_unstructured_logs.ts new file mode 100644 index 0000000000000..83860635ae64a --- /dev/null +++ b/packages/kbn-apm-synthtrace/src/scenarios/distributed_unstructured_logs.ts @@ -0,0 +1,197 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the "Elastic License + * 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side + * Public License v 1"; you may not use this file except in compliance with, at + * your election, the "Elastic License 2.0", the "GNU Affero General Public + * License v3.0 only", or the "Server Side Public License, v 1". + */ + +import { infra, LogDocument, log } from '@kbn/apm-synthtrace-client'; +import { fakerEN as faker } from '@faker-js/faker'; +import { z } from '@kbn/zod'; +import { Scenario } from '../cli/scenario'; +import { withClient } from '../lib/utils/with_client'; +import { + LogMessageGenerator, + generateUnstructuredLogMessage, + unstructuredLogMessageGenerators, +} from './helpers/unstructured_logs'; + +const scenarioOptsSchema = z.intersection( + z.object({ + randomSeed: z.number().default(0), + messageGroup: z + .enum([ + 'httpAccess', + 'userAuthentication', + 'networkEvent', + 'dbOperations', + 'taskOperations', + 'degradedOperations', + 'errorOperations', + ]) + .default('dbOperations'), + }), + z + .discriminatedUnion('distribution', [ + z.object({ + distribution: z.literal('uniform'), + rate: z.number().default(1), + }), + z.object({ + distribution: z.literal('poisson'), + rate: z.number().default(1), + }), + z.object({ + distribution: z.literal('gaussian'), + mean: z.coerce.date().describe('Time of the peak of the gaussian distribution'), + width: z.number().default(5000).describe('Width of the gaussian distribution in ms'), + totalPoints: z + .number() + .default(100) + .describe('Total number of points in the gaussian distribution'), + }), + ]) + .default({ distribution: 'uniform', rate: 1 }) +); + +type ScenarioOpts = z.output; + +const scenario: Scenario = async (runOptions) => { + return { + generate: ({ range, clients: { logsEsClient } }) => { + const { logger } = runOptions; + const scenarioOpts = scenarioOptsSchema.parse(runOptions.scenarioOpts ?? {}); + + faker.seed(scenarioOpts.randomSeed); + faker.setDefaultRefDate(range.from.toISOString()); + + logger.debug(`Generating ${scenarioOpts.distribution} logs...`); + + // Logs Data logic + const LOG_LEVELS = ['info', 'debug', 'error', 'warn', 'trace', 'fatal']; + + const clusterDefinions = [ + { + 'orchestrator.cluster.id': faker.string.nanoid(), + 'orchestrator.cluster.name': 'synth-cluster-1', + 'orchestrator.namespace': 'default', + 'cloud.provider': 'gcp', + 'cloud.region': 'eu-central-1', + 'cloud.availability_zone': 'eu-central-1a', + 'cloud.project.id': faker.string.nanoid(), + }, + { + 'orchestrator.cluster.id': faker.string.nanoid(), + 'orchestrator.cluster.name': 'synth-cluster-2', + 'orchestrator.namespace': 'production', + 'cloud.provider': 'aws', + 'cloud.region': 'us-east-1', + 'cloud.availability_zone': 'us-east-1a', + 'cloud.project.id': faker.string.nanoid(), + }, + { + 'orchestrator.cluster.id': faker.string.nanoid(), + 'orchestrator.cluster.name': 'synth-cluster-3', + 'orchestrator.namespace': 'kube', + 'cloud.provider': 'azure', + 'cloud.region': 'area-51', + 'cloud.availability_zone': 'area-51a', + 'cloud.project.id': faker.string.nanoid(), + }, + ]; + + const hostEntities = [ + { + 'host.name': 'host-1', + 'agent.id': 'synth-agent-1', + 'agent.name': 'nodejs', + 'cloud.instance.id': faker.string.nanoid(), + 'orchestrator.resource.id': faker.string.nanoid(), + ...clusterDefinions[0], + }, + { + 'host.name': 'host-2', + 'agent.id': 'synth-agent-2', + 'agent.name': 'custom', + 'cloud.instance.id': faker.string.nanoid(), + 'orchestrator.resource.id': faker.string.nanoid(), + ...clusterDefinions[1], + }, + { + 'host.name': 'host-3', + 'agent.id': 'synth-agent-3', + 'agent.name': 'python', + 'cloud.instance.id': faker.string.nanoid(), + 'orchestrator.resource.id': faker.string.nanoid(), + ...clusterDefinions[2], + }, + ].map((hostDefinition) => + infra.minimalHost(hostDefinition['host.name']).overrides(hostDefinition) + ); + + const serviceNames = Array(3) + .fill(null) + .map((_, idx) => `synth-service-${idx}`); + + const generatorFactory = + scenarioOpts.distribution === 'uniform' + ? range.interval('1s').rate(scenarioOpts.rate) + : scenarioOpts.distribution === 'poisson' + ? range.poissonEvents(scenarioOpts.rate) + : range.gaussianEvents(scenarioOpts.mean, scenarioOpts.width, scenarioOpts.totalPoints); + + const logs = generatorFactory.generator((timestamp) => { + const entity = faker.helpers.arrayElement(hostEntities); + const serviceName = faker.helpers.arrayElement(serviceNames); + const level = faker.helpers.arrayElement(LOG_LEVELS); + const messages = logMessageGenerators[scenarioOpts.messageGroup](faker); + + return messages.map((message) => + log + .createMinimal() + .message(message) + .logLevel(level) + .service(serviceName) + .overrides({ + ...entity.fields, + labels: { + scenario: 'rare', + population: scenarioOpts.distribution, + }, + }) + .timestamp(timestamp) + ); + }); + + return [ + withClient( + logsEsClient, + logger.perf('generating_logs', () => [logs]) + ), + ]; + }, + }; +}; + +export default scenario; + +const logMessageGenerators = { + httpAccess: generateUnstructuredLogMessage([unstructuredLogMessageGenerators.httpAccess]), + userAuthentication: generateUnstructuredLogMessage([ + unstructuredLogMessageGenerators.userAuthentication, + ]), + networkEvent: generateUnstructuredLogMessage([unstructuredLogMessageGenerators.networkEvent]), + dbOperations: generateUnstructuredLogMessage([unstructuredLogMessageGenerators.dbOperation]), + taskOperations: generateUnstructuredLogMessage([ + unstructuredLogMessageGenerators.taskStatusSuccess, + ]), + degradedOperations: generateUnstructuredLogMessage([ + unstructuredLogMessageGenerators.taskStatusFailure, + ]), + errorOperations: generateUnstructuredLogMessage([ + unstructuredLogMessageGenerators.error, + unstructuredLogMessageGenerators.restart, + ]), +} satisfies Record; diff --git a/packages/kbn-apm-synthtrace/src/scenarios/failed_logs.ts b/packages/kbn-apm-synthtrace/src/scenarios/failed_logs.ts new file mode 100644 index 0000000000000..91ddedac270b5 --- /dev/null +++ b/packages/kbn-apm-synthtrace/src/scenarios/failed_logs.ts @@ -0,0 +1,195 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the "Elastic License + * 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side + * Public License v 1"; you may not use this file except in compliance with, at + * your election, the "Elastic License 2.0", the "GNU Affero General Public + * License v3.0 only", or the "Server Side Public License, v 1". + */ + +import { LogDocument, log, generateShortId, generateLongId } from '@kbn/apm-synthtrace-client'; +import { merge } from 'lodash'; +import { Scenario } from '../cli/scenario'; +import { IndexTemplateName } from '../lib/logs/custom_logsdb_index_templates'; +import { withClient } from '../lib/utils/with_client'; +import { + getServiceName, + getCluster, + getCloudRegion, + getCloudProvider, + MORE_THAN_1024_CHARS, +} from './helpers/logs_mock_data'; +import { parseLogsScenarioOpts } from './helpers/logs_scenario_opts_parser'; +import { LogsIndex } from '../lib/logs/logs_synthtrace_es_client'; + +const processors = [ + { + script: { + tag: 'normalize log level', + lang: 'painless', + source: ` + String level = ctx['log.level']; + if ('0'.equals(level)) { + ctx['log.level'] = 'info'; + } else if ('1'.equals(level)) { + ctx['log.level'] = 'debug'; + } else if ('2'.equals(level)) { + ctx['log.level'] = 'warning'; + } else if ('3'.equals(level)) { + ctx['log.level'] = 'error'; + } else { + throw new Exception("Not a valid log level"); + } + `, + }, + }, +]; + +// Logs Data logic +const MESSAGE_LOG_LEVELS = [ + { message: 'A simple log', level: '0' }, + { + message: 'Another log message', + level: '1', + }, + { + message: 'A log message generated from a warning', + level: '2', + }, + { message: 'Error with certificate: "ca_trusted_fingerprint"', level: '3' }, +]; + +const scenario: Scenario = async (runOptions) => { + const { isLogsDb } = parseLogsScenarioOpts(runOptions.scenarioOpts); + return { + bootstrap: async ({ logsEsClient }) => { + await logsEsClient.createCustomPipeline(processors); + if (isLogsDb) await logsEsClient.createIndexTemplate(IndexTemplateName.LogsDb); + + await logsEsClient.updateIndexTemplate( + isLogsDb ? IndexTemplateName.LogsDb : LogsIndex, + (template) => { + const next = { + name: LogsIndex, + data_stream: { + failure_store: true, + }, + }; + + return merge({}, template, next); + } + ); + }, + generate: ({ range, clients: { logsEsClient } }) => { + const { logger } = runOptions; + + const constructLogsCommonData = () => { + const index = Math.floor(Math.random() * 3); + const serviceName = getServiceName(index); + const logMessage = MESSAGE_LOG_LEVELS[index]; + const { clusterId, clusterName } = getCluster(index); + const cloudRegion = getCloudRegion(index); + + const commonLongEntryFields: LogDocument = { + 'trace.id': generateShortId(), + 'agent.name': 'synth-agent', + 'orchestrator.cluster.name': clusterName, + 'orchestrator.cluster.id': clusterId, + 'orchestrator.resource.id': generateShortId(), + 'cloud.provider': getCloudProvider(), + 'cloud.region': cloudRegion, + 'cloud.availability_zone': `${cloudRegion}a`, + 'cloud.project.id': generateShortId(), + 'cloud.instance.id': generateShortId(), + 'log.file.path': `/logs/${generateLongId()}/error.txt`, + }; + + return { + index, + serviceName, + logMessage, + cloudRegion, + commonLongEntryFields, + }; + }; + + const datasetSynth1Logs = (timestamp: number) => { + const { + serviceName, + logMessage: { level, message }, + commonLongEntryFields, + } = constructLogsCommonData(); + + return log + .create({ isLogsDb }) + .dataset('synth.1') + .message(message) + .logLevel(level) + .service(serviceName) + .defaults(commonLongEntryFields) + .timestamp(timestamp); + }; + + const datasetSynth2Logs = (i: number, timestamp: number) => { + const { + serviceName, + logMessage: { level, message }, + commonLongEntryFields, + } = constructLogsCommonData(); + const isFailed = i % 60 === 0; + return log + .create({ isLogsDb }) + .dataset('synth.2') + .message(message) + .logLevel(isFailed ? '4' : level) // "script_exception": Not a valid log level + .service(serviceName) + .defaults(commonLongEntryFields) + .timestamp(timestamp); + }; + + const datasetSynth3Logs = (i: number, timestamp: number) => { + const { + serviceName, + logMessage: { level, message }, + cloudRegion, + commonLongEntryFields, + } = constructLogsCommonData(); + const isMalformed = i % 10 === 0; + const isFailed = i % 80 === 0; + return log + .create({ isLogsDb }) + .dataset('synth.3') + .message(message) + .logLevel(isFailed ? '5' : level) // "script_exception": Not a valid log level + .service(serviceName) + .defaults({ + ...commonLongEntryFields, + 'cloud.availability_zone': isMalformed + ? MORE_THAN_1024_CHARS // "ignore_above": 1024 in mapping + : `${cloudRegion}a`, + }) + .timestamp(timestamp); + }; + + const logs = range + .interval('1m') + .rate(1) + .generator((timestamp) => { + return Array(200) + .fill(0) + .flatMap((_, index) => [ + datasetSynth1Logs(timestamp), + datasetSynth2Logs(index, timestamp), + datasetSynth3Logs(index, timestamp), + ]); + }); + + return withClient( + logsEsClient, + logger.perf('generating_logs', () => logs) + ); + }, + }; +}; + +export default scenario; diff --git a/packages/kbn-apm-synthtrace/src/scenarios/helpers/logs_mock_data.ts b/packages/kbn-apm-synthtrace/src/scenarios/helpers/logs_mock_data.ts index e974528f16a80..5f3cbd5f054dd 100644 --- a/packages/kbn-apm-synthtrace/src/scenarios/helpers/logs_mock_data.ts +++ b/packages/kbn-apm-synthtrace/src/scenarios/helpers/logs_mock_data.ts @@ -59,6 +59,9 @@ const SERVICE_NAMES = Array(3) .fill(null) .map((_, idx) => `synth-service-${idx}`); +export const MORE_THAN_1024_CHARS = + 'Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum. Sed ut perspiciatis unde omnis iste natus error sit voluptatem accusantium doloremque laudantium, totam rem aperiam, eaque ipsa quae ab illo inventore veritatis et quasi architecto beatae vitae dicta sunt explicabo. Nemo enim ipsam voluptatem quia voluptas sit aspernatur aut odit aut fugit, sed quia consequuntur magni dolores eos qui ratione voluptatem sequi nesciunt. Neque porro quisquam est, qui dolorem ipsum quia dolor sit amet, consectetur, adipisci velit, sed quia non numquam eius modi tempora incidunt ut labore et dolore magnam aliquam quaerat voluptatem. Ut enim ad minima veniam, quis nostrum exercitationem ullam corporis suscipit laboriosam, nisi ut aliquid ex ea commodi consequatur? Quis autem vel eum iure reprehenderit qui in ea voluptate velit esse quam nihil molestiae consequatur, vel illum qui dolorem eum fugiat quo voluptas nulla pariatur?'; + // Functions to get random elements export const getCluster = (index?: number) => getAtIndexOrRandom(CLUSTER, index); export const getIpAddress = (index?: number) => getAtIndexOrRandom(IP_ADDRESSES, index); diff --git a/packages/kbn-apm-synthtrace/src/scenarios/helpers/unstructured_logs.ts b/packages/kbn-apm-synthtrace/src/scenarios/helpers/unstructured_logs.ts new file mode 100644 index 0000000000000..490bd449e2b60 --- /dev/null +++ b/packages/kbn-apm-synthtrace/src/scenarios/helpers/unstructured_logs.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", the "GNU Affero General Public License v3.0 only", and the "Server Side + * Public License v 1"; you may not use this file except in compliance with, at + * your election, the "Elastic License 2.0", the "GNU Affero General Public + * License v3.0 only", or the "Server Side Public License, v 1". + */ + +import { Faker, faker } from '@faker-js/faker'; + +export type LogMessageGenerator = (f: Faker) => string[]; + +export const unstructuredLogMessageGenerators = { + httpAccess: (f: Faker) => [ + `${f.internet.ip()} - - [${f.date + .past() + .toISOString() + .replace('T', ' ') + .replace( + /\..+/, + '' + )}] "${f.internet.httpMethod()} ${f.internet.url()} HTTP/1.1" ${f.helpers.arrayElement([ + 200, 301, 404, 500, + ])} ${f.number.int({ min: 100, max: 5000 })}`, + ], + dbOperation: (f: Faker) => [ + `${f.database.engine()}: ${f.database.column()} ${f.helpers.arrayElement([ + 'created', + 'updated', + 'deleted', + 'inserted', + ])} successfully ${f.number.int({ max: 100000 })} times`, + ], + taskStatusSuccess: (f: Faker) => [ + `${f.hacker.noun()}: ${f.word.words()} ${f.helpers.arrayElement([ + 'triggered', + 'executed', + 'processed', + 'handled', + ])} successfully at ${f.date.recent().toISOString()}`, + ], + taskStatusFailure: (f: Faker) => [ + `${f.hacker.noun()}: ${f.helpers.arrayElement([ + 'triggering', + 'execution', + 'processing', + 'handling', + ])} of ${f.word.words()} failed at ${f.date.recent().toISOString()}`, + ], + error: (f: Faker) => [ + `${f.helpers.arrayElement([ + 'Error', + 'Exception', + 'Failure', + 'Crash', + 'Bug', + 'Issue', + ])}: ${f.hacker.phrase()}`, + `Stopping ${f.number.int(42)} background tasks...`, + 'Shutting down process...', + ], + restart: (f: Faker) => { + const service = f.database.engine(); + return [ + `Restarting ${service}...`, + `Waiting for queue to drain...`, + `Service ${service} restarted ${f.helpers.arrayElement([ + 'successfully', + 'with errors', + 'with warnings', + ])}`, + ]; + }, + userAuthentication: (f: Faker) => [ + `User ${f.internet.userName()} ${f.helpers.arrayElement([ + 'logged in', + 'logged out', + 'failed to login', + ])}`, + ], + networkEvent: (f: Faker) => [ + `Network ${f.helpers.arrayElement([ + 'connection', + 'disconnection', + 'data transfer', + ])} ${f.helpers.arrayElement(['from', 'to'])} ${f.internet.ip()}`, + ], +} satisfies Record; + +export const generateUnstructuredLogMessage = + (generators: LogMessageGenerator[] = Object.values(unstructuredLogMessageGenerators)) => + (f: Faker = faker) => + f.helpers.arrayElement(generators)(f); diff --git a/packages/kbn-apm-synthtrace/src/scenarios/logs_traces_hosts.ts b/packages/kbn-apm-synthtrace/src/scenarios/logs_traces_hosts.ts index 8a6bdf409a573..6dac3fc9f3226 100644 --- a/packages/kbn-apm-synthtrace/src/scenarios/logs_traces_hosts.ts +++ b/packages/kbn-apm-synthtrace/src/scenarios/logs_traces_hosts.ts @@ -8,21 +8,22 @@ */ import { - log, - LogDocument, + ApmFields, InfraDocument, - apm, Instance, - infra, - ApmFields, + LogDocument, + apm, generateShortId, + infra, + log, } from '@kbn/apm-synthtrace-client'; import { Scenario } from '../cli/scenario'; +import { IndexTemplateName } from '../lib/logs/custom_logsdb_index_templates'; import { Logger } from '../lib/utils/create_logger'; -import { withClient } from '../lib/utils/with_client'; import { getSynthtraceEnvironment } from '../lib/utils/get_synthtrace_environment'; +import { withClient } from '../lib/utils/with_client'; +import { MORE_THAN_1024_CHARS } from './helpers/logs_mock_data'; import { parseLogsScenarioOpts, parseStringToBoolean } from './helpers/logs_scenario_opts_parser'; -import { IndexTemplateName } from '../lib/logs/custom_logsdb_index_templates'; const ENVIRONMENT = getSynthtraceEnvironment(__filename); @@ -475,6 +476,3 @@ const DATASETS = [ ]; const LOG_LEVELS = ['info', 'error', 'warn', 'debug']; - -const MORE_THAN_1024_CHARS = - 'Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum. Sed ut perspiciatis unde omnis iste natus error sit voluptatem accusantium doloremque laudantium, totam rem aperiam, eaque ipsa quae ab illo inventore veritatis et quasi architecto beatae vitae dicta sunt explicabo. Nemo enim ipsam voluptatem quia voluptas sit aspernatur aut odit aut fugit, sed quia consequuntur magni dolores eos qui ratione voluptatem sequi nesciunt. Neque porro quisquam est, qui dolorem ipsum quia dolor sit amet, consectetur, adipisci velit, sed quia non numquam eius modi tempora incidunt ut labore et dolore magnam aliquam quaerat voluptatem. Ut enim ad minima veniam, quis nostrum exercitationem ullam corporis suscipit laboriosam, nisi ut aliquid ex ea commodi consequatur? Quis autem vel eum iure reprehenderit qui in ea voluptate velit esse quam nihil molestiae consequatur, vel illum qui dolorem eum fugiat quo voluptas nulla pariatur?'; diff --git a/packages/kbn-apm-synthtrace/src/scenarios/simple_logs.ts b/packages/kbn-apm-synthtrace/src/scenarios/simple_logs.ts index 3c1fdc5131395..08d914c1017dd 100644 --- a/packages/kbn-apm-synthtrace/src/scenarios/simple_logs.ts +++ b/packages/kbn-apm-synthtrace/src/scenarios/simple_logs.ts @@ -7,19 +7,20 @@ * License v3.0 only", or the "Server Side Public License, v 1". */ -import { LogDocument, log, generateShortId, generateLongId } from '@kbn/apm-synthtrace-client'; +import { LogDocument, generateLongId, generateShortId, log } from '@kbn/apm-synthtrace-client'; import moment from 'moment'; import { Scenario } from '../cli/scenario'; import { IndexTemplateName } from '../lib/logs/custom_logsdb_index_templates'; import { withClient } from '../lib/utils/with_client'; import { - getServiceName, - getGeoCoordinate, - getIpAddress, - getCluster, + MORE_THAN_1024_CHARS, + getAgentName, getCloudProvider, getCloudRegion, - getAgentName, + getCluster, + getGeoCoordinate, + getIpAddress, + getServiceName, } from './helpers/logs_mock_data'; import { parseLogsScenarioOpts } from './helpers/logs_scenario_opts_parser'; @@ -30,9 +31,6 @@ const MESSAGE_LOG_LEVELS = [ { message: 'Error with certificate: "ca_trusted_fingerprint"', level: 'error' }, ]; -const MORE_THAN_1024_CHARS = - 'Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum. Sed ut perspiciatis unde omnis iste natus error sit voluptatem accusantium doloremque laudantium, totam rem aperiam, eaque ipsa quae ab illo inventore veritatis et quasi architecto beatae vitae dicta sunt explicabo. Nemo enim ipsam voluptatem quia voluptas sit aspernatur aut odit aut fugit, sed quia consequuntur magni dolores eos qui ratione voluptatem sequi nesciunt. Neque porro quisquam est, qui dolorem ipsum quia dolor sit amet, consectetur, adipisci velit, sed quia non numquam eius modi tempora incidunt ut labore et dolore magnam aliquam quaerat voluptatem. Ut enim ad minima veniam, quis nostrum exercitationem ullam corporis suscipit laboriosam, nisi ut aliquid ex ea commodi consequatur? Quis autem vel eum iure reprehenderit qui in ea voluptate velit esse quam nihil molestiae consequatur, vel illum qui dolorem eum fugiat quo voluptas nulla pariatur?'; - const scenario: Scenario = async (runOptions) => { const { isLogsDb } = parseLogsScenarioOpts(runOptions.scenarioOpts); diff --git a/packages/kbn-apm-synthtrace/tsconfig.json b/packages/kbn-apm-synthtrace/tsconfig.json index d0f5c5801597a..db93e36421b83 100644 --- a/packages/kbn-apm-synthtrace/tsconfig.json +++ b/packages/kbn-apm-synthtrace/tsconfig.json @@ -10,6 +10,7 @@ "@kbn/apm-synthtrace-client", "@kbn/dev-utils", "@kbn/elastic-agent-utils", + "@kbn/zod", ], "exclude": [ "target/**/*", diff --git a/packages/kbn-esql-ast/src/pretty_print/__tests__/wrapping_pretty_printer.test.ts b/packages/kbn-esql-ast/src/pretty_print/__tests__/wrapping_pretty_printer.test.ts index 21330d0fea3b1..2dfe239ce5b88 100644 --- a/packages/kbn-esql-ast/src/pretty_print/__tests__/wrapping_pretty_printer.test.ts +++ b/packages/kbn-esql-ast/src/pretty_print/__tests__/wrapping_pretty_printer.test.ts @@ -593,6 +593,85 @@ ROW (asdf + asdf)::string, 1.2::string, "1234"::integer, (12321342134 + 23412341 - "aaaaaaaaaaa")::boolean`); }); }); + + describe('list literals', () => { + describe('numeric', () => { + test('wraps long list literals one line', () => { + const query = + 'ROW [1234567890, 1234567890, 1234567890, 1234567890, 1234567890, 1234567890, 1234567890, 1234567890, 1234567890]'; + const text = reprint(query).text; + + expect('\n' + text).toBe(` +ROW + [1234567890, 1234567890, 1234567890, 1234567890, 1234567890, 1234567890, + 1234567890, 1234567890, 1234567890]`); + }); + + test('wraps long list literals to multiple lines one line', () => { + const query = `ROW [1234567890, 1234567890, 1234567890, 1234567890, 1234567890, 1234567890, + 1234567890, 1234567890, 1234567890, 1234567890, 1234567890, 1234567890, + 1234567890, 1234567890, 1234567890, 1234567890, 1234567890, 1234567890, + 1234567890, 1234567890, 1234567890]`; + const text = reprint(query).text; + + expect('\n' + text).toBe(` +ROW + [1234567890, 1234567890, 1234567890, 1234567890, 1234567890, 1234567890, + 1234567890, 1234567890, 1234567890, 1234567890, 1234567890, 1234567890, + 1234567890, 1234567890, 1234567890, 1234567890, 1234567890, 1234567890, + 1234567890, 1234567890, 1234567890]`); + }); + + test('breaks very long values one-per-line', () => { + const query = `ROW fn1(fn2(fn3(fn4(fn5(fn6(fn7(fn8([1234567890, 1234567890, 1234567890, 1234567890, 1234567890]))))))))`; + const text = reprint(query, { wrap: 40 }).text; + + expect('\n' + text).toBe(` +ROW + FN1( + FN2( + FN3( + FN4( + FN5( + FN6( + FN7( + FN8( + [ + 1234567890, + 1234567890, + 1234567890, + 1234567890, + 1234567890]))))))))`); + }); + }); + + describe('string', () => { + test('wraps long list literals one line', () => { + const query = + 'ROW ["some text", "another text", "one more text literal", "and another one", "and one more", "and one more", "and one more", "and one more", "and one more"]'; + const text = reprint(query).text; + + expect('\n' + text).toBe(` +ROW + ["some text", "another text", "one more text literal", "and another one", + "and one more", "and one more", "and one more", "and one more", + "and one more"]`); + }); + + test('can break very long strings per line', () => { + const query = + 'ROW ["..............................................", "..............................................", ".............................................."]'; + const text = reprint(query).text; + + expect('\n' + text).toBe(` +ROW + [ + "..............................................", + "..............................................", + ".............................................."]`); + }); + }); + }); }); test.todo('Idempotence on multiple times pretty printing'); diff --git a/packages/kbn-esql-ast/src/pretty_print/wrapping_pretty_printer.ts b/packages/kbn-esql-ast/src/pretty_print/wrapping_pretty_printer.ts index fde7f60a1dba5..91f65a389f0c3 100644 --- a/packages/kbn-esql-ast/src/pretty_print/wrapping_pretty_printer.ts +++ b/packages/kbn-esql-ast/src/pretty_print/wrapping_pretty_printer.ts @@ -15,9 +15,10 @@ import { CommandVisitorContext, ExpressionVisitorContext, FunctionCallExpressionVisitorContext, + ListLiteralExpressionVisitorContext, Visitor, } from '../visitor'; -import { singleItems } from '../visitor/utils'; +import { children, singleItems } from '../visitor/utils'; import { BasicPrettyPrinter, BasicPrettyPrinterOptions } from './basic_pretty_printer'; import { getPrettyPrintStats } from './helpers'; import { LeafPrinter } from './leaf_printer'; @@ -235,7 +236,11 @@ export class WrappingPrettyPrinter { } private printArguments( - ctx: CommandVisitorContext | CommandOptionVisitorContext | FunctionCallExpressionVisitorContext, + ctx: + | CommandVisitorContext + | CommandOptionVisitorContext + | FunctionCallExpressionVisitorContext + | ListLiteralExpressionVisitorContext, inp: Input ) { let txt = ''; @@ -247,7 +252,7 @@ export class WrappingPrettyPrinter { let remainingCurrentLine = inp.remaining; let oneArgumentPerLine = false; - for (const child of singleItems(ctx.node.args)) { + for (const child of children(ctx.node)) { if (getPrettyPrintStats(child).hasLineBreakingDecorations) { oneArgumentPerLine = true; break; @@ -489,13 +494,11 @@ export class WrappingPrettyPrinter { }) .on('visitListLiteralExpression', (ctx, inp: Input): Output => { - let elements = ''; - - for (const out of ctx.visitElements(inp)) { - elements += (elements ? ', ' : '') + out.txt; - } - - const formatted = `[${elements}]${inp.suffix ?? ''}`; + const args = this.printArguments(ctx, { + indent: inp.indent, + remaining: inp.remaining - 1, + }); + const formatted = `[${args.txt}]${inp.suffix ?? ''}`; const { txt, indented } = this.decorateWithComments(inp.indent, ctx.node, formatted); return { txt, indented }; diff --git a/packages/kbn-esql-ast/src/types.ts b/packages/kbn-esql-ast/src/types.ts index 0ca48b2326f7d..1bac6e0cff5b3 100644 --- a/packages/kbn-esql-ast/src/types.ts +++ b/packages/kbn-esql-ast/src/types.ts @@ -40,6 +40,7 @@ export type ESQLAstField = ESQLFunction | ESQLColumn; export type ESQLAstItem = ESQLSingleAstItem | ESQLAstItem[]; export type ESQLAstNodeWithArgs = ESQLCommand | ESQLCommandOption | ESQLFunction; +export type ESQLAstNodeWithChildren = ESQLAstNodeWithArgs | ESQLList; /** * *Proper* are nodes which are objects with `type` property, once we get rid diff --git a/packages/kbn-esql-ast/src/visitor/contexts.ts b/packages/kbn-esql-ast/src/visitor/contexts.ts index 0f637962b7ddd..4b4f04fdca4bb 100644 --- a/packages/kbn-esql-ast/src/visitor/contexts.ts +++ b/packages/kbn-esql-ast/src/visitor/contexts.ts @@ -12,11 +12,12 @@ // and makes it harder to understand the code structure. import { type GlobalVisitorContext, SharedData } from './global_visitor_context'; -import { firstItem, singleItems } from './utils'; +import { children, firstItem, singleItems } from './utils'; import type { ESQLAstCommand, ESQLAstItem, ESQLAstNodeWithArgs, + ESQLAstNodeWithChildren, ESQLAstRenameExpression, ESQLColumn, ESQLCommandOption, @@ -47,6 +48,11 @@ import { Builder } from '../builder'; const isNodeWithArgs = (x: unknown): x is ESQLAstNodeWithArgs => !!x && typeof x === 'object' && Array.isArray((x as any).args); +const isNodeWithChildren = (x: unknown): x is ESQLAstNodeWithChildren => + !!x && + typeof x === 'object' && + (Array.isArray((x as any).args) || Array.isArray((x as any).values)); + export class VisitorContext< Methods extends VisitorMethods = VisitorMethods, Data extends SharedData = SharedData, @@ -99,13 +105,13 @@ export class VisitorContext< public arguments(): ESQLAstExpressionNode[] { const node = this.node; - if (!isNodeWithArgs(node)) { + if (!isNodeWithChildren(node)) { return []; } const args: ESQLAstExpressionNode[] = []; - for (const arg of singleItems(node.args)) { + for (const arg of children(node)) { args.push(arg); } diff --git a/packages/kbn-esql-ast/src/visitor/utils.ts b/packages/kbn-esql-ast/src/visitor/utils.ts index 2e54a89c2bf52..0dc95b73cf9d7 100644 --- a/packages/kbn-esql-ast/src/visitor/utils.ts +++ b/packages/kbn-esql-ast/src/visitor/utils.ts @@ -7,7 +7,7 @@ * License v3.0 only", or the "Server Side Public License, v 1". */ -import { ESQLAstItem, ESQLSingleAstItem } from '../types'; +import { ESQLAstItem, ESQLProperNode, ESQLSingleAstItem } from '../types'; /** * Normalizes AST "item" list to only contain *single* items. @@ -48,3 +48,32 @@ export const lastItem = (items: ESQLAstItem[]): ESQLSingleAstItem | undefined => if (Array.isArray(last)) return lastItem(last as ESQLAstItem[]); return last as ESQLSingleAstItem; }; + +export function* children(node: ESQLProperNode): Iterable { + switch (node.type) { + case 'function': + case 'command': + case 'option': { + for (const arg of singleItems(node.args)) { + yield arg; + } + break; + } + case 'list': { + for (const item of singleItems(node.values)) { + yield item; + } + break; + } + case 'inlineCast': { + if (Array.isArray(node.value)) { + for (const item of singleItems(node.value)) { + yield item; + } + } else { + yield node.value; + } + break; + } + } +} diff --git a/packages/kbn-esql-utils/index.ts b/packages/kbn-esql-utils/index.ts index 223181f2bd154..333557964d873 100644 --- a/packages/kbn-esql-utils/index.ts +++ b/packages/kbn-esql-utils/index.ts @@ -29,6 +29,7 @@ export { isQueryWrappedByPipes, retrieveMetadataColumns, getQueryColumnsFromESQLQuery, + isESQLColumnSortable, TextBasedLanguages, } from './src'; diff --git a/packages/kbn-esql-utils/src/index.ts b/packages/kbn-esql-utils/src/index.ts index e36283c7a9238..3b3228e7a2a4a 100644 --- a/packages/kbn-esql-utils/src/index.ts +++ b/packages/kbn-esql-utils/src/index.ts @@ -31,3 +31,4 @@ export { getStartEndParams, hasStartEndParams, } from './utils/run_query'; +export { isESQLColumnSortable } from './utils/esql_fields_utils'; diff --git a/packages/kbn-esql-utils/src/utils/esql_fields_utils.test.ts b/packages/kbn-esql-utils/src/utils/esql_fields_utils.test.ts new file mode 100644 index 0000000000000..ef8a24e686bd6 --- /dev/null +++ b/packages/kbn-esql-utils/src/utils/esql_fields_utils.test.ts @@ -0,0 +1,66 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the "Elastic License + * 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side + * Public License v 1"; you may not use this file except in compliance with, at + * your election, the "Elastic License 2.0", the "GNU Affero General Public + * License v3.0 only", or the "Server Side Public License, v 1". + */ +import type { DatatableColumn } from '@kbn/expressions-plugin/common'; +import { isESQLColumnSortable } from './esql_fields_utils'; + +describe('esql fields helpers', () => { + describe('isESQLColumnSortable', () => { + it('returns false for geo fields', () => { + const geoField = { + id: 'geo.coordinates', + name: 'geo.coordinates', + meta: { + type: 'geo_point', + esType: 'geo_point', + }, + isNull: false, + } as DatatableColumn; + expect(isESQLColumnSortable(geoField)).toBeFalsy(); + }); + + it('returns false for source fields', () => { + const sourceField = { + id: '_source', + name: '_source', + meta: { + type: '_source', + esType: '_source', + }, + isNull: false, + } as DatatableColumn; + expect(isESQLColumnSortable(sourceField)).toBeFalsy(); + }); + + it('returns false for counter fields', () => { + const tsdbField = { + id: 'tsbd_counter', + name: 'tsbd_counter', + meta: { + type: 'number', + esType: 'counter_long', + }, + isNull: false, + } as DatatableColumn; + expect(isESQLColumnSortable(tsdbField)).toBeFalsy(); + }); + + it('returns true for everything else', () => { + const keywordField = { + id: 'sortable', + name: 'sortable', + meta: { + type: 'string', + esType: 'keyword', + }, + isNull: false, + } as DatatableColumn; + expect(isESQLColumnSortable(keywordField)).toBeTruthy(); + }); + }); +}); diff --git a/packages/kbn-esql-utils/src/utils/esql_fields_utils.ts b/packages/kbn-esql-utils/src/utils/esql_fields_utils.ts new file mode 100644 index 0000000000000..f5a0fe7b81340 --- /dev/null +++ b/packages/kbn-esql-utils/src/utils/esql_fields_utils.ts @@ -0,0 +1,40 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the "Elastic License + * 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side + * Public License v 1"; you may not use this file except in compliance with, at + * your election, the "Elastic License 2.0", the "GNU Affero General Public + * License v3.0 only", or the "Server Side Public License, v 1". + */ + +import type { DatatableColumn } from '@kbn/expressions-plugin/common'; + +const SPATIAL_FIELDS = ['geo_point', 'geo_shape', 'point', 'shape']; +const SOURCE_FIELD = '_source'; +const TSDB_COUNTER_FIELDS_PREFIX = 'counter_'; + +/** + * Check if a column is sortable. + * + * @param column The DatatableColumn of the field. + * @returns True if the column is sortable, false otherwise. + */ + +export const isESQLColumnSortable = (column: DatatableColumn): boolean => { + // We don't allow sorting on spatial fields + if (SPATIAL_FIELDS.includes(column.meta?.type)) { + return false; + } + + // we don't allow sorting on the _source field + if (column.meta?.type === SOURCE_FIELD) { + return false; + } + + // we don't allow sorting on tsdb counter fields + if (column.meta?.esType && column.meta?.esType?.indexOf(TSDB_COUNTER_FIELDS_PREFIX) !== -1) { + return false; + } + + return true; +}; diff --git a/packages/kbn-esql-validation-autocomplete/src/autocomplete/autocomplete.test.ts b/packages/kbn-esql-validation-autocomplete/src/autocomplete/autocomplete.test.ts index 84779f1dd36b5..a0a4a359c5ff6 100644 --- a/packages/kbn-esql-validation-autocomplete/src/autocomplete/autocomplete.test.ts +++ b/packages/kbn-esql-validation-autocomplete/src/autocomplete/autocomplete.test.ts @@ -387,6 +387,23 @@ describe('autocomplete', () => { '```````````````````````````````round(doubleField) + 1```````````````` + 1```````` + 1```` + 1`` + 1`', ] ); + + it('should not suggest already-used fields and variables', async () => { + const { suggest: suggestTest } = await setup(); + const getSuggestions = async (query: string) => + (await suggestTest(query)).map((value) => value.text); + + expect(await getSuggestions('from a_index | EVAL foo = 1 | KEEP /')).toContain('foo'); + expect(await getSuggestions('from a_index | EVAL foo = 1 | KEEP foo, /')).not.toContain( + 'foo' + ); + expect(await getSuggestions('from a_index | EVAL foo = 1 | KEEP /')).toContain( + 'doubleField' + ); + expect( + await getSuggestions('from a_index | EVAL foo = 1 | KEEP doubleField, /') + ).not.toContain('doubleField'); + }); }); } @@ -1111,11 +1128,14 @@ describe('autocomplete', () => { ]); }); - describe('KEEP ', () => { + describe.each(['KEEP', 'DROP'])('%s ', (commandName) => { // KEEP field - testSuggestions('FROM a | KEEP /', getFieldNamesByType('any').map(attachTriggerCommand)); testSuggestions( - 'FROM a | KEEP d/', + `FROM a | ${commandName} /`, + getFieldNamesByType('any').map(attachTriggerCommand) + ); + testSuggestions( + `FROM a | ${commandName} d/`, getFieldNamesByType('any') .map((text) => ({ text, @@ -1124,11 +1144,11 @@ describe('autocomplete', () => { .map(attachTriggerCommand) ); testSuggestions( - 'FROM a | KEEP doubleFiel/', + `FROM a | ${commandName} doubleFiel/`, getFieldNamesByType('any').map(attachTriggerCommand) ); testSuggestions( - 'FROM a | KEEP doubleField/', + `FROM a | ${commandName} doubleField/`, ['doubleField, ', 'doubleField | '] .map((text) => ({ text, @@ -1141,7 +1161,7 @@ describe('autocomplete', () => { // Let's get funky with the field names testSuggestions( - 'FROM a | KEEP @timestamp/', + `FROM a | ${commandName} @timestamp/`, ['@timestamp, ', '@timestamp | '] .map((text) => ({ text, @@ -1150,10 +1170,15 @@ describe('autocomplete', () => { })) .map(attachTriggerCommand), undefined, - [[{ name: '@timestamp', type: 'date' }]] + [ + [ + { name: '@timestamp', type: 'date' }, + { name: 'utc_stamp', type: 'date' }, + ], + ] ); testSuggestions( - 'FROM a | KEEP foo.bar/', + `FROM a | ${commandName} foo.bar/`, ['foo.bar, ', 'foo.bar | '] .map((text) => ({ text, @@ -1162,26 +1187,34 @@ describe('autocomplete', () => { })) .map(attachTriggerCommand), undefined, - [[{ name: 'foo.bar', type: 'double' }]] + [ + [ + { name: 'foo.bar', type: 'double' }, + { name: 'baz', type: 'date' }, + ], + ] ); describe('escaped field names', () => { // This isn't actually the behavior we want, but this test is here // to make sure no weird suggestions start cropping up in this case. - testSuggestions('FROM a | KEEP `foo.bar`/', ['foo.bar'], undefined, [ + testSuggestions(`FROM a | ${commandName} \`foo.bar\`/`, ['foo.bar'], undefined, [ [{ name: 'foo.bar', type: 'double' }], ]); // @todo re-enable these tests when we can use AST to support this case - testSuggestions.skip('FROM a | KEEP `foo.bar`/', ['foo.bar, ', 'foo.bar | '], undefined, [ - [{ name: 'foo.bar', type: 'double' }], - ]); testSuggestions.skip( - 'FROM a | KEEP `foo`.`bar`/', + `FROM a | ${commandName} \`foo.bar\`/`, ['foo.bar, ', 'foo.bar | '], undefined, [[{ name: 'foo.bar', type: 'double' }]] ); - testSuggestions.skip('FROM a | KEEP `any#Char$Field`/', [ + testSuggestions.skip( + `FROM a | ${commandName} \`foo\`.\`bar\`/`, + ['foo.bar, ', 'foo.bar | '], + undefined, + [[{ name: 'foo.bar', type: 'double' }]] + ); + testSuggestions.skip(`FROM a | ${commandName} \`any#Char$Field\`/`, [ '`any#Char$Field`, ', '`any#Char$Field` | ', ]); @@ -1189,12 +1222,28 @@ describe('autocomplete', () => { // Subsequent fields testSuggestions( - 'FROM a | KEEP doubleField, dateFiel/', + `FROM a | ${commandName} doubleField, dateFiel/`, getFieldNamesByType('any') .filter((s) => s !== 'doubleField') .map(attachTriggerCommand) ); - testSuggestions('FROM a | KEEP doubleField, dateField/', ['dateField, ', 'dateField | ']); + testSuggestions(`FROM a | ${commandName} doubleField, dateField/`, [ + 'dateField, ', + 'dateField | ', + ]); + + // out of fields + testSuggestions( + `FROM a | ${commandName} doubleField, dateField/`, + ['dateField | '], + undefined, + [ + [ + { name: 'doubleField', type: 'double' }, + { name: 'dateField', type: 'date' }, + ], + ] + ); }); }); }); diff --git a/packages/kbn-esql-validation-autocomplete/src/autocomplete/autocomplete.ts b/packages/kbn-esql-validation-autocomplete/src/autocomplete/autocomplete.ts index 2433f5d496521..6f9fb66a8c715 100644 --- a/packages/kbn-esql-validation-autocomplete/src/autocomplete/autocomplete.ts +++ b/packages/kbn-esql-validation-autocomplete/src/autocomplete/autocomplete.ts @@ -627,7 +627,7 @@ async function getExpressionSuggestionsByType( literals: argDef.constantOnly, }, { - ignoreFields: isNewExpression + ignoreColumns: isNewExpression ? command.args.filter(isColumnItem).map(({ name }) => name) : [], } @@ -656,10 +656,15 @@ async function getExpressionSuggestionsByType( })); } - return [ - { ...pipeCompleteItem, text: ' | ' }, - { ...commaCompleteItem, text: ', ' }, - ].map((s) => ({ + const finalSuggestions = [{ ...pipeCompleteItem, text: ' | ' }]; + if (fieldSuggestions.length > 1) + // when we fix the editor marker, this should probably be checked against 0 instead of 1 + // this is because the last field in the AST is currently getting removed (because it contains + // the editor marker) so it is not included in the ignored list which is used to filter out + // existing fields above. + finalSuggestions.push({ ...commaCompleteItem, text: ', ' }); + + return finalSuggestions.map((s) => ({ ...s, filterText: fragment, text: fragment + s.text, @@ -1176,15 +1181,15 @@ async function getFieldsOrFunctionsSuggestions( }, { ignoreFn = [], - ignoreFields = [], + ignoreColumns = [], }: { ignoreFn?: string[]; - ignoreFields?: string[]; + ignoreColumns?: string[]; } = {} ): Promise { const filteredFieldsByType = pushItUpInTheList( (await (fields - ? getFieldsByType(types, ignoreFields, { + ? getFieldsByType(types, ignoreColumns, { advanceCursor: commandName === 'sort', openSuggestions: commandName === 'sort', }) @@ -1195,7 +1200,10 @@ async function getFieldsOrFunctionsSuggestions( const filteredVariablesByType: string[] = []; if (variables) { for (const variable of variables.values()) { - if (types.includes('any') || types.includes(variable[0].type)) { + if ( + (types.includes('any') || types.includes(variable[0].type)) && + !ignoreColumns.includes(variable[0].name) + ) { filteredVariablesByType.push(variable[0].name); } } @@ -1515,7 +1523,7 @@ async function getListArgsSuggestions( fields: true, variables: anyVariables, }, - { ignoreFields: [firstArg.name, ...otherArgs.map(({ name }) => name)] } + { ignoreColumns: [firstArg.name, ...otherArgs.map(({ name }) => name)] } )) ); } @@ -1875,18 +1883,16 @@ async function getOptionArgsSuggestions( * for a given fragment of text in a generic way. A good example is * a field name. * - * When typing a field name, there are three scenarios + * When typing a field name, there are 2 scenarios * - * 1. user hasn't begun typing + * 1. field name is incomplete (includes the empty string) * KEEP / - * - * 2. user is typing a partial field name * KEEP fie/ * - * 3. user has typed a complete field name + * 2. field name is complete * KEEP field/ * - * This function provides a framework for handling all three scenarios in a clean way. + * This function provides a framework for detecting and handling both scenarios in a clean way. * * @param innerText - the query text before the current cursor position * @param isFragmentComplete — return true if the fragment is complete diff --git a/packages/kbn-import-resolver/src/import_resolver.ts b/packages/kbn-import-resolver/src/import_resolver.ts index 1b41418a5cb24..9ca16981b2afc 100644 --- a/packages/kbn-import-resolver/src/import_resolver.ts +++ b/packages/kbn-import-resolver/src/import_resolver.ts @@ -122,11 +122,6 @@ export class ImportResolver { return true; } - // ignore amd require done by ace syntax plugin - if (req === 'ace/lib/dom') { - return true; - } - // typescript validates these imports fine and they're purely virtual thanks to ambient type definitions in @elastic/eui so /shrug if ( req.startsWith('@elastic/eui/src/components/') || diff --git a/packages/kbn-import-resolver/src/integration_tests/import_resolver.test.ts b/packages/kbn-import-resolver/src/integration_tests/import_resolver.test.ts index f484de7904f06..1089f811b6e98 100644 --- a/packages/kbn-import-resolver/src/integration_tests/import_resolver.test.ts +++ b/packages/kbn-import-resolver/src/integration_tests/import_resolver.test.ts @@ -99,12 +99,6 @@ describe('#resolve()', () => { } `); - expect(resolver.resolve('ace/lib/dom', FIXTURES_DIR)).toMatchInlineSnapshot(` - Object { - "type": "ignore", - } - `); - expect(resolver.resolve('@elastic/eui/src/components/foo', FIXTURES_DIR)) .toMatchInlineSnapshot(` Object { diff --git a/packages/kbn-management/settings/setting_ids/index.ts b/packages/kbn-management/settings/setting_ids/index.ts index cb32dbd4a4505..e926007f77f25 100644 --- a/packages/kbn-management/settings/setting_ids/index.ts +++ b/packages/kbn-management/settings/setting_ids/index.ts @@ -142,8 +142,10 @@ export const OBSERVABILITY_APM_ENABLE_SERVICE_INVENTORY_TABLE_SEARCH_BAR = 'observability:apmEnableServiceInventoryTableSearchBar'; export const OBSERVABILITY_LOGS_EXPLORER_ALLOWED_DATA_VIEWS_ID = 'observability:logsExplorer:allowedDataViews'; +export const OBSERVABILITY_LOGS_SHARED_NEW_LOGS_OVERVIEW_ID = 'observability:newLogsOverview'; export const OBSERVABILITY_ENTITY_CENTRIC_EXPERIENCE = 'observability:entityCentricExperience'; export const OBSERVABILITY_LOGS_DATA_ACCESS_LOG_SOURCES_ID = 'observability:logSources'; +export const OBSERVABILITY_ENABLE_LOGS_STREAM = 'observability:enableLogsStream'; export const OBSERVABILITY_AI_ASSISTANT_SIMULATED_FUNCTION_CALLING = 'observability:aiAssistantSimulatedFunctionCalling'; export const OBSERVABILITY_AI_ASSISTANT_SEARCH_CONNECTOR_INDEX_PATTERN = @@ -177,13 +179,9 @@ export const SECURITY_SOLUTION_RULES_TABLE_REFRESH_ID = 'securitySolution:rulesT export const SECURITY_SOLUTION_ENABLE_NEWS_FEED_ID = 'securitySolution:enableNewsFeed'; export const SECURITY_SOLUTION_NEWS_FEED_URL_ID = 'securitySolution:newsFeedUrl'; export const SECURITY_SOLUTION_IP_REPUTATION_LINKS_ID = 'securitySolution:ipReputationLinks'; -export const SECURITY_SOLUTION_ENABLE_CCS_WARNING_ID = 'securitySolution:enableCcsWarning'; export const SECURITY_SOLUTION_SHOW_RELATED_INTEGRATIONS_ID = 'securitySolution:showRelatedIntegrations'; export const SECURITY_SOLUTION_DEFAULT_ALERT_TAGS_KEY = 'securitySolution:alertTags' as const; -/** This Kibana Advanced Setting allows users to enable/disable querying cold and frozen data tiers in analyzer */ -export const SECURITY_SOLUTION_EXCLUDE_COLD_AND_FROZEN_TIERS_IN_ANALYZER = - 'securitySolution:excludeColdAndFrozenTiersInAnalyzer' as const; /** This Kibana Advanced Setting allows users to enable/disable the Asset Criticality feature */ export const SECURITY_SOLUTION_ENABLE_ASSET_CRITICALITY_SETTING = 'securitySolution:enableAssetCriticality' as const; diff --git a/packages/kbn-mock-idp-plugin/public/login_page.tsx b/packages/kbn-mock-idp-plugin/public/login_page.tsx index eeb51e3aa37c1..92561ca51f584 100644 --- a/packages/kbn-mock-idp-plugin/public/login_page.tsx +++ b/packages/kbn-mock-idp-plugin/public/login_page.tsx @@ -7,21 +7,24 @@ * License v3.0 only", or the "Server Side Public License, v 1". */ +import type { EuiComboBoxOptionOption } from '@elastic/eui'; import { EuiButton, - EuiPageTemplate, - EuiEmptyPrompt, + EuiButtonEmpty, EuiComboBox, - EuiInlineEditTitle, + EuiEmptyPrompt, EuiFormRow, + EuiInlineEditTitle, + EuiPageTemplate, EuiSpacer, - EuiComboBoxOptionOption, - EuiButtonEmpty, } from '@elastic/eui'; -import React, { ChangeEvent, useEffect, useState, useRef } from 'react'; -import { FormikProvider, useFormik, Field, Form } from 'formik'; +import { Field, Form, FormikProvider, useFormik } from 'formik'; +import type { ChangeEvent } from 'react'; +import React, { useEffect, useRef, useState } from 'react'; + +import type { CoreStart } from '@kbn/core-lifecycle-browser'; import { useKibana } from '@kbn/kibana-react-plugin/public'; -import { CoreStart } from '@kbn/core-lifecycle-browser'; + import { useAuthenticator } from './role_switcher'; export const LoginPage = () => { diff --git a/packages/kbn-mock-idp-plugin/public/plugin.tsx b/packages/kbn-mock-idp-plugin/public/plugin.tsx index 0d168a5d6ec32..c1f733027f656 100644 --- a/packages/kbn-mock-idp-plugin/public/plugin.tsx +++ b/packages/kbn-mock-idp-plugin/public/plugin.tsx @@ -7,14 +7,16 @@ * License v3.0 only", or the "Server Side Public License, v 1". */ -import type { PluginInitializer } from '@kbn/core-plugins-browser'; import React from 'react'; import ReactDOM from 'react-dom'; -import { KibanaThemeProvider } from '@kbn/react-kibana-context-theme'; -import { KibanaContextProvider } from '@kbn/kibana-react-plugin/public'; + +import type { CloudSetup, CloudStart } from '@kbn/cloud-plugin/public'; +import type { PluginInitializer } from '@kbn/core-plugins-browser'; import { I18nProvider } from '@kbn/i18n-react'; +import { KibanaContextProvider } from '@kbn/kibana-react-plugin/public'; import { MOCK_IDP_LOGIN_PATH } from '@kbn/mock-idp-utils/src/constants'; -import type { CloudStart, CloudSetup } from '@kbn/cloud-plugin/public'; +import { KibanaThemeProvider } from '@kbn/react-kibana-context-theme'; + import { RoleSwitcher } from './role_switcher'; export interface PluginSetupDependencies { diff --git a/packages/kbn-mock-idp-plugin/public/reload_page_toast.tsx b/packages/kbn-mock-idp-plugin/public/reload_page_toast.tsx index 8f21570555626..c3f93f3269da1 100644 --- a/packages/kbn-mock-idp-plugin/public/reload_page_toast.tsx +++ b/packages/kbn-mock-idp-plugin/public/reload_page_toast.tsx @@ -7,13 +7,13 @@ * License v3.0 only", or the "Server Side Public License, v 1". */ +import { EuiButton, EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; import React from 'react'; -import { toMountPoint } from '@kbn/react-kibana-mount'; -import { EuiButton, EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; -import type { ToastInput } from '@kbn/core-notifications-browser'; import type { I18nStart } from '@kbn/core-i18n-browser'; +import type { ToastInput } from '@kbn/core-notifications-browser'; import type { ThemeServiceStart } from '@kbn/core-theme-browser'; +import { toMountPoint } from '@kbn/react-kibana-mount'; import type { AuthenticatedUser } from '@kbn/security-plugin-types-common'; export const DATA_TEST_SUBJ_PAGE_RELOAD_BUTTON = 'pageReloadButton'; diff --git a/packages/kbn-mock-idp-plugin/public/role_switcher.tsx b/packages/kbn-mock-idp-plugin/public/role_switcher.tsx index d0ffa3a67a697..347293abbc6c7 100644 --- a/packages/kbn-mock-idp-plugin/public/role_switcher.tsx +++ b/packages/kbn-mock-idp-plugin/public/role_switcher.tsx @@ -7,13 +7,15 @@ * License v3.0 only", or the "Server Side Public License, v 1". */ +import { EuiButton, EuiContextMenu, EuiPopover } from '@elastic/eui'; import React, { useEffect, useState } from 'react'; -import { EuiButton, EuiPopover, EuiContextMenu } from '@elastic/eui'; -import { useKibana } from '@kbn/kibana-react-plugin/public'; -import { CoreStart } from '@kbn/core-lifecycle-browser'; import useAsyncFn from 'react-use/lib/useAsyncFn'; -import type { AuthenticatedUser } from '@kbn/security-plugin-types-common'; + +import type { CoreStart } from '@kbn/core-lifecycle-browser'; +import { useKibana } from '@kbn/kibana-react-plugin/public'; import { MOCK_IDP_REALM_NAME, MOCK_IDP_REALM_TYPE } from '@kbn/mock-idp-utils/src/constants'; +import type { AuthenticatedUser } from '@kbn/security-plugin-types-common'; + import { createReloadPageToast } from './reload_page_toast'; import type { CreateSAMLResponseParams } from '../server'; diff --git a/packages/kbn-mock-idp-plugin/server/plugin.ts b/packages/kbn-mock-idp-plugin/server/plugin.ts index 27d5b96a4cbfc..fc9043099b197 100644 --- a/packages/kbn-mock-idp-plugin/server/plugin.ts +++ b/packages/kbn-mock-idp-plugin/server/plugin.ts @@ -7,17 +7,18 @@ * License v3.0 only", or the "Server Side Public License, v 1". */ -import type { PluginInitializer, Plugin } from '@kbn/core-plugins-server'; +import { resolve } from 'path'; + +import type { CloudSetup } from '@kbn/cloud-plugin/server'; import { schema } from '@kbn/config-schema'; import type { TypeOf } from '@kbn/config-schema'; -import { MOCK_IDP_LOGIN_PATH, MOCK_IDP_LOGOUT_PATH, createSAMLResponse } from '@kbn/mock-idp-utils'; +import type { Plugin, PluginInitializer } from '@kbn/core-plugins-server'; import { + readRolesFromResource, SERVERLESS_ROLES_ROOT_PATH, STATEFUL_ROLES_ROOT_PATH, - readRolesFromResource, } from '@kbn/es'; -import { resolve } from 'path'; -import { CloudSetup } from '@kbn/cloud-plugin/server'; +import { createSAMLResponse, MOCK_IDP_LOGIN_PATH, MOCK_IDP_LOGOUT_PATH } from '@kbn/mock-idp-utils'; export interface PluginSetupDependencies { cloud: CloudSetup; diff --git a/packages/kbn-mock-idp-utils/src/utils.ts b/packages/kbn-mock-idp-utils/src/utils.ts index 85d37fdce5ab5..af07d69cfe936 100644 --- a/packages/kbn-mock-idp-utils/src/utils.ts +++ b/packages/kbn-mock-idp-utils/src/utils.ts @@ -7,23 +7,23 @@ * License v3.0 only", or the "Server Side Public License, v 1". */ -import { Client } from '@elastic/elasticsearch'; - -import { SignedXml } from 'xml-crypto'; -import { KBN_KEY_PATH, KBN_CERT_PATH } from '@kbn/dev-utils'; -import { readFile } from 'fs/promises'; +import type { Client } from '@elastic/elasticsearch'; import { X509Certificate } from 'crypto'; +import { readFile } from 'fs/promises'; +import { SignedXml } from 'xml-crypto'; + +import { KBN_CERT_PATH, KBN_KEY_PATH } from '@kbn/dev-utils'; import { - MOCK_IDP_REALM_NAME, - MOCK_IDP_ENTITY_ID, - MOCK_IDP_ROLE_MAPPING_NAME, - MOCK_IDP_ATTRIBUTE_PRINCIPAL, - MOCK_IDP_ATTRIBUTE_ROLES, MOCK_IDP_ATTRIBUTE_EMAIL, MOCK_IDP_ATTRIBUTE_NAME, + MOCK_IDP_ATTRIBUTE_PRINCIPAL, + MOCK_IDP_ATTRIBUTE_ROLES, + MOCK_IDP_ENTITY_ID, MOCK_IDP_LOGIN_PATH, MOCK_IDP_LOGOUT_PATH, + MOCK_IDP_REALM_NAME, + MOCK_IDP_ROLE_MAPPING_NAME, } from './constants'; /** diff --git a/packages/kbn-optimizer/src/worker/webpack.config.ts b/packages/kbn-optimizer/src/worker/webpack.config.ts index 539d3098030e0..52a837724480d 100644 --- a/packages/kbn-optimizer/src/worker/webpack.config.ts +++ b/packages/kbn-optimizer/src/worker/webpack.config.ts @@ -247,6 +247,18 @@ export function getWebpackConfig( }, }, }, + { + test: /node_modules\/@?xstate5\/.*\.js$/, + use: { + loader: 'babel-loader', + options: { + babelrc: false, + envName: worker.dist ? 'production' : 'development', + presets: [BABEL_PRESET], + plugins: ['@babel/plugin-transform-logical-assignment-operators'], + }, + }, + }, { test: /\.(html|md|txt|tmpl)$/, use: { diff --git a/packages/kbn-router-to-openapispec/index.ts b/packages/kbn-router-to-openapispec/index.ts index 17f8253348ab3..1869167db0323 100644 --- a/packages/kbn-router-to-openapispec/index.ts +++ b/packages/kbn-router-to-openapispec/index.ts @@ -7,4 +7,7 @@ * License v3.0 only", or the "Server Side Public License, v 1". */ -export { generateOpenApiDocument } from './src/generate_oas'; +export { + generateOpenApiDocument, + type GenerateOpenApiDocumentOptionsFilters, +} from './src/generate_oas'; diff --git a/packages/kbn-router-to-openapispec/src/util.test.ts b/packages/kbn-router-to-openapispec/src/util.test.ts index 79b4ddf8eba84..abbb605df79e5 100644 --- a/packages/kbn-router-to-openapispec/src/util.test.ts +++ b/packages/kbn-router-to-openapispec/src/util.test.ts @@ -163,6 +163,15 @@ describe('prepareRoutes', () => { output: [{ path: '/api/foo', options: { access: pub } }], filters: { excludePathsMatching: ['/api/b'], access: pub }, }, + { + input: [ + { path: '/api/foo', options: { access: pub, excludeFromOAS: true } }, + { path: '/api/bar', options: { access: internal } }, + { path: '/api/baz', options: { access: pub } }, + ], + output: [{ path: '/api/baz', options: { access: pub } }], + filters: { excludePathsMatching: ['/api/bar'], access: pub }, + }, ])('returns the expected routes #%#', ({ input, output, filters }) => { expect(prepareRoutes(input, filters)).toEqual(output); }); diff --git a/packages/kbn-router-to-openapispec/src/util.ts b/packages/kbn-router-to-openapispec/src/util.ts index 1aa2a080ccc18..55f7348dc199a 100644 --- a/packages/kbn-router-to-openapispec/src/util.ts +++ b/packages/kbn-router-to-openapispec/src/util.ts @@ -105,13 +105,14 @@ export const getVersionedHeaderParam = ( }); export const prepareRoutes = < - R extends { path: string; options: { access?: 'public' | 'internal' } } + R extends { path: string; options: { access?: 'public' | 'internal'; excludeFromOAS?: boolean } } >( routes: R[], filters: GenerateOpenApiDocumentOptionsFilters = {} ): R[] => { if (Object.getOwnPropertyNames(filters).length === 0) return routes; return routes.filter((route) => { + if (route.options.excludeFromOAS) return false; if ( filters.excludePathsMatching && filters.excludePathsMatching.some((ex) => route.path.startsWith(ex)) diff --git a/packages/kbn-search-connectors/components/configuration/connector_configuration_form.tsx b/packages/kbn-search-connectors/components/configuration/connector_configuration_form.tsx index e70754d5e09e8..f7e619f407f12 100644 --- a/packages/kbn-search-connectors/components/configuration/connector_configuration_form.tsx +++ b/packages/kbn-search-connectors/components/configuration/connector_configuration_form.tsx @@ -109,6 +109,15 @@ export const ConnectorConfigurationForm: React.FC = items={category.configEntries} hasDocumentLevelSecurityEnabled={hasDocumentLevelSecurity} setConfigEntry={(key, value) => { + const entry = localConfig[key]; + if (entry && !isCategoryEntry(entry)) { + const newConfiguration: ConnectorConfiguration = { + ...localConfig, + [key]: { ...entry, value }, + }; + setLocalConfig(newConfiguration); + } + const categories = configView.categories; categories[index] = { ...categories[index], [key]: value }; setConfigView({ @@ -136,6 +145,15 @@ export const ConnectorConfigurationForm: React.FC = items={configView.advancedConfigurations} hasDocumentLevelSecurityEnabled={hasDocumentLevelSecurity} setConfigEntry={(key, value) => { + const entry = localConfig[key]; + if (entry && !isCategoryEntry(entry)) { + const newConfiguration: ConnectorConfiguration = { + ...localConfig, + [key]: { ...entry, value }, + }; + setLocalConfig(newConfiguration); + } + setConfigView({ ...configView, advancedConfigurations: configView.advancedConfigurations.map((config) => diff --git a/packages/kbn-storybook/src/webpack.config.ts b/packages/kbn-storybook/src/webpack.config.ts index fb901692e7f66..b03d78dbbc190 100644 --- a/packages/kbn-storybook/src/webpack.config.ts +++ b/packages/kbn-storybook/src/webpack.config.ts @@ -125,6 +125,17 @@ export default ({ config: storybookConfig }: { config: Configuration }) => { }, ], }, + { + test: /node_modules\/@?xstate5\/.*\.js$/, + use: { + loader: 'babel-loader', + options: { + babelrc: false, + presets: [require.resolve('@kbn/babel-preset/webpack_preset')], + plugins: ['@babel/plugin-transform-logical-assignment-operators'], + }, + }, + }, ], }, plugins: [new IgnoreNotFoundExportPlugin()], diff --git a/packages/kbn-test/src/jest/resolver.js b/packages/kbn-test/src/jest/resolver.js index 27e0b14876587..8f985e9463962 100644 --- a/packages/kbn-test/src/jest/resolver.js +++ b/packages/kbn-test/src/jest/resolver.js @@ -70,7 +70,7 @@ module.exports = (request, options) => { return FILE_MOCK; } - if (reqExt === '.worker' && (reqBasename.endsWith('.ace') || reqBasename.endsWith('.editor'))) { + if (reqExt === '.worker' && reqBasename.endsWith('.editor')) { return WORKER_MOCK; } } diff --git a/packages/kbn-ui-shared-deps-npm/BUILD.bazel b/packages/kbn-ui-shared-deps-npm/BUILD.bazel index 48f234b0bfe10..ad3f3474f1b4e 100644 --- a/packages/kbn-ui-shared-deps-npm/BUILD.bazel +++ b/packages/kbn-ui-shared-deps-npm/BUILD.bazel @@ -53,7 +53,6 @@ RUNTIME_DEPS = [ "@npm//jquery", "@npm//lodash", "@npm//moment-timezone", - "@npm//react-ace", "@npm//react-dom", "@npm//react-router-dom", "@npm//react-router-dom-v5-compat", diff --git a/packages/kbn-ui-shared-deps-npm/webpack.config.js b/packages/kbn-ui-shared-deps-npm/webpack.config.js index 3b16430aeb724..926a041a72c3d 100644 --- a/packages/kbn-ui-shared-deps-npm/webpack.config.js +++ b/packages/kbn-ui-shared-deps-npm/webpack.config.js @@ -88,7 +88,6 @@ module.exports = (_, argv) => { 'moment-timezone/moment-timezone', 'moment-timezone/data/packed/latest.json', 'moment', - 'react-ace', 'react-dom', 'react-dom/server', 'react-router-dom', diff --git a/packages/kbn-user-profile-components/src/hooks/use_update_user_profile.test.tsx b/packages/kbn-user-profile-components/src/hooks/use_update_user_profile.test.tsx index 0837292d36ac6..87d41bd222637 100644 --- a/packages/kbn-user-profile-components/src/hooks/use_update_user_profile.test.tsx +++ b/packages/kbn-user-profile-components/src/hooks/use_update_user_profile.test.tsx @@ -7,8 +7,8 @@ * License v3.0 only", or the "Server Side Public License, v 1". */ -import React from 'react'; import { act, renderHook, type WrapperComponent } from '@testing-library/react-hooks'; +import React from 'react'; import { BehaviorSubject, first, lastValueFrom, of } from 'rxjs'; import { coreMock } from '@kbn/core/public/mocks'; diff --git a/packages/kbn-user-profile-components/src/hooks/use_update_user_profile.tsx b/packages/kbn-user-profile-components/src/hooks/use_update_user_profile.tsx index 8c276dc533f6c..57aeec7a51d5a 100644 --- a/packages/kbn-user-profile-components/src/hooks/use_update_user_profile.tsx +++ b/packages/kbn-user-profile-components/src/hooks/use_update_user_profile.tsx @@ -8,13 +8,14 @@ */ import { EuiButton, EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; -import React, { useCallback, useRef, useState, useEffect } from 'react'; +import { merge } from 'lodash'; +import React, { useCallback, useEffect, useRef, useState } from 'react'; import useObservable from 'react-use/lib/useObservable'; + import { i18n } from '@kbn/i18n'; -import { merge } from 'lodash'; -import type { UserProfileData } from '../types'; import { useUserProfiles } from '../services'; +import type { UserProfileData } from '../types'; interface Props { notificationSuccess?: { @@ -74,23 +75,25 @@ export const useUpdateUserProfile = ({ { title: notificationTitle, text: ( - - -

{pageReloadText}

- window.location.reload()} - data-test-subj="windowReloadButton" - > - {i18n.translate( - 'userProfileComponents.updateUserProfile.notification.requiresPageReloadButtonLabel', - { - defaultMessage: 'Reload page', - } - )} - -
-
+ <> +

{pageReloadText}

+ + + window.location.reload()} + data-test-subj="windowReloadButton" + > + {i18n.translate( + 'userProfileComponents.updateUserProfile.notification.requiresPageReloadButtonLabel', + { + defaultMessage: 'Reload page', + } + )} + + + + ), }, { diff --git a/packages/kbn-user-profile-components/src/services.tsx b/packages/kbn-user-profile-components/src/services.tsx index 726814f370412..7cf7a2d66c82f 100644 --- a/packages/kbn-user-profile-components/src/services.tsx +++ b/packages/kbn-user-profile-components/src/services.tsx @@ -7,12 +7,13 @@ * License v3.0 only", or the "Server Side Public License, v 1". */ -import React, { FC, PropsWithChildren, useContext } from 'react'; +import type { FC, PropsWithChildren } from 'react'; +import React, { useContext } from 'react'; import type { I18nStart } from '@kbn/core-i18n-browser'; import type { NotificationsStart, ToastOptions } from '@kbn/core-notifications-browser'; import type { ThemeServiceStart } from '@kbn/core-theme-browser'; -import { toMountPoint } from '@kbn/react-kibana-mount'; +import type { toMountPoint } from '@kbn/react-kibana-mount'; import type { UserProfileAPIClient } from './types'; diff --git a/packages/kbn-user-profile-components/src/types.ts b/packages/kbn-user-profile-components/src/types.ts index abdc9fba618ad..ff74061e0ef39 100644 --- a/packages/kbn-user-profile-components/src/types.ts +++ b/packages/kbn-user-profile-components/src/types.ts @@ -7,7 +7,7 @@ * License v3.0 only", or the "Server Side Public License, v 1". */ -import { Observable } from 'rxjs'; +import type { Observable } from 'rxjs'; /** * Avatar stored in user profile. diff --git a/packages/kbn-user-profile-components/src/user_avatar.tsx b/packages/kbn-user-profile-components/src/user_avatar.tsx index 2b55e7909ef8c..5c31d7112afde 100644 --- a/packages/kbn-user-profile-components/src/user_avatar.tsx +++ b/packages/kbn-user-profile-components/src/user_avatar.tsx @@ -11,8 +11,8 @@ import type { EuiAvatarProps } from '@elastic/eui'; import { EuiAvatar, useEuiTheme } from '@elastic/eui'; import type { FunctionComponent } from 'react'; import React from 'react'; -import { UserProfileAvatarData } from './types'; +import type { UserProfileAvatarData } from './types'; import type { UserProfile, UserProfileUserInfo } from './user_profile'; import { getUserAvatarColor, diff --git a/packages/kbn-user-profile-components/src/user_profile.ts b/packages/kbn-user-profile-components/src/user_profile.ts index 082f6d11d7ec0..7c029f606fa7f 100644 --- a/packages/kbn-user-profile-components/src/user_profile.ts +++ b/packages/kbn-user-profile-components/src/user_profile.ts @@ -8,6 +8,7 @@ */ import { VISUALIZATION_COLORS } from '@elastic/eui'; + import type { UserProfileAvatarData, UserProfileData } from './types'; /** diff --git a/packages/kbn-user-profile-components/src/user_profiles_popover.test.tsx b/packages/kbn-user-profile-components/src/user_profiles_popover.test.tsx index 743dbf3232114..69d33763ba82b 100644 --- a/packages/kbn-user-profile-components/src/user_profiles_popover.test.tsx +++ b/packages/kbn-user-profile-components/src/user_profiles_popover.test.tsx @@ -9,8 +9,8 @@ import { shallow } from 'enzyme'; import React from 'react'; -import { UserProfile } from './user_profile'; +import type { UserProfile } from './user_profile'; import { UserProfilesPopover } from './user_profiles_popover'; const userProfiles: UserProfile[] = [ diff --git a/packages/kbn-user-profile-components/src/user_profiles_popover.tsx b/packages/kbn-user-profile-components/src/user_profiles_popover.tsx index 197f6952110dd..decd3607133ab 100644 --- a/packages/kbn-user-profile-components/src/user_profiles_popover.tsx +++ b/packages/kbn-user-profile-components/src/user_profiles_popover.tsx @@ -7,12 +7,13 @@ * License v3.0 only", or the "Server Side Public License, v 1". */ -import type { EuiPopoverProps, EuiContextMenuPanelProps } from '@elastic/eui'; +import type { EuiContextMenuPanelProps, EuiPopoverProps } from '@elastic/eui'; +import { EuiContextMenuPanel, EuiPopover, useGeneratedHtmlId } from '@elastic/eui'; import React from 'react'; -import { EuiPopover, EuiContextMenuPanel, useGeneratedHtmlId } from '@elastic/eui'; -import { UserProfilesSelectable, UserProfilesSelectableProps } from './user_profiles_selectable'; import type { UserProfileWithAvatar } from './user_avatar'; +import type { UserProfilesSelectableProps } from './user_profiles_selectable'; +import { UserProfilesSelectable } from './user_profiles_selectable'; /** * Props of {@link UserProfilesPopover} component diff --git a/packages/kbn-user-profile-components/src/user_profiles_selectable.test.tsx b/packages/kbn-user-profile-components/src/user_profiles_selectable.test.tsx index 8ba8fe6e61d07..3bd720e97d96d 100644 --- a/packages/kbn-user-profile-components/src/user_profiles_selectable.test.tsx +++ b/packages/kbn-user-profile-components/src/user_profiles_selectable.test.tsx @@ -7,10 +7,11 @@ * License v3.0 only", or the "Server Side Public License, v 1". */ -import { mountWithIntl as mount } from '@kbn/test-jest-helpers'; import React from 'react'; -import { UserProfile } from './user_profile'; +import { mountWithIntl as mount } from '@kbn/test-jest-helpers'; + +import type { UserProfile } from './user_profile'; import { UserProfilesSelectable } from './user_profiles_selectable'; const userProfiles: UserProfile[] = [ diff --git a/packages/kbn-user-profile-components/src/user_profiles_selectable.tsx b/packages/kbn-user-profile-components/src/user_profiles_selectable.tsx index 8dadde4427739..b6684f6e1fdb9 100644 --- a/packages/kbn-user-profile-components/src/user_profiles_selectable.tsx +++ b/packages/kbn-user-profile-components/src/user_profiles_selectable.tsx @@ -10,15 +10,15 @@ import type { EuiSelectableOption, EuiSelectableProps } from '@elastic/eui'; import { EuiButtonEmpty, + EuiCallOut, EuiFlexGroup, EuiFlexItem, + EuiHighlight, EuiHorizontalRule, EuiPanel, EuiSelectable, EuiSpacer, EuiText, - EuiCallOut, - EuiHighlight, } from '@elastic/eui'; import type { ReactNode } from 'react'; import React, { useEffect, useState } from 'react'; @@ -26,9 +26,9 @@ import React, { useEffect, useState } from 'react'; import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n-react'; -import { getUserDisplayLabel, getUserDisplayName } from './user_profile'; import type { UserProfileWithAvatar } from './user_avatar'; import { UserAvatar } from './user_avatar'; +import { getUserDisplayLabel, getUserDisplayName } from './user_profile'; const NULL_OPTION_KEY = 'null'; diff --git a/packages/kbn-user-profile-components/src/user_tooltip.tsx b/packages/kbn-user-profile-components/src/user_tooltip.tsx index 040e497adb497..80739d40802f5 100644 --- a/packages/kbn-user-profile-components/src/user_tooltip.tsx +++ b/packages/kbn-user-profile-components/src/user_tooltip.tsx @@ -7,15 +7,15 @@ * License v3.0 only", or the "Server Side Public License, v 1". */ -import { EuiText, EuiToolTipProps } from '@elastic/eui'; -import { EuiToolTip, EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; +import type { EuiToolTipProps } from '@elastic/eui'; +import { EuiFlexGroup, EuiFlexItem, EuiText, EuiToolTip } from '@elastic/eui'; import type { FunctionComponent } from 'react'; import React from 'react'; -import type { UserProfileUserInfo } from './user_profile'; +import type { UserProfileAvatarData } from './types'; import { UserAvatar } from './user_avatar'; +import type { UserProfileUserInfo } from './user_profile'; import { getUserDisplayName } from './user_profile'; -import { UserProfileAvatarData } from './types'; /** * Props of {@link UserToolTip} component diff --git a/packages/kbn-xstate-utils/kibana.jsonc b/packages/kbn-xstate-utils/kibana.jsonc index cd1151a3f2103..1fb3507854b98 100644 --- a/packages/kbn-xstate-utils/kibana.jsonc +++ b/packages/kbn-xstate-utils/kibana.jsonc @@ -1,5 +1,5 @@ { - "type": "shared-common", + "type": "shared-browser", "id": "@kbn/xstate-utils", "owner": "@elastic/obs-ux-logs-team" } diff --git a/packages/kbn-xstate-utils/src/console_inspector.ts b/packages/kbn-xstate-utils/src/console_inspector.ts new file mode 100644 index 0000000000000..8792ab44f3c28 --- /dev/null +++ b/packages/kbn-xstate-utils/src/console_inspector.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", the "GNU Affero General Public License v3.0 only", and the "Server Side + * Public License v 1"; you may not use this file except in compliance with, at + * your election, the "Elastic License 2.0", the "GNU Affero General Public + * License v3.0 only", or the "Server Side Public License, v 1". + */ + +import { + ActorRefLike, + AnyActorRef, + InspectedActorEvent, + InspectedEventEvent, + InspectedSnapshotEvent, + InspectionEvent, +} from 'xstate5'; +import { isDevMode } from './dev_tools'; + +export const createConsoleInspector = () => { + if (!isDevMode()) { + return () => {}; + } + + // eslint-disable-next-line no-console + const log = console.info.bind(console); + + const logActorEvent = (actorEvent: InspectedActorEvent) => { + if (isActorRef(actorEvent.actorRef)) { + log( + '✨ %c%s%c is a new actor of type %c%s%c:', + ...styleAsActor(actorEvent.actorRef.id), + ...styleAsKeyword(actorEvent.type), + actorEvent.actorRef + ); + } else { + log('✨ New %c%s%c actor without id:', ...styleAsKeyword(actorEvent.type), actorEvent); + } + }; + + const logEventEvent = (eventEvent: InspectedEventEvent) => { + if (isActorRef(eventEvent.actorRef)) { + log( + '🔔 %c%s%c received event %c%s%c from %c%s%c:', + ...styleAsActor(eventEvent.actorRef.id), + ...styleAsKeyword(eventEvent.event.type), + ...styleAsKeyword(eventEvent.sourceRef?.id), + eventEvent + ); + } else { + log('🔔 Event', ...styleAsKeyword(eventEvent.event.type), ':', eventEvent); + } + }; + + const logSnapshotEvent = (snapshotEvent: InspectedSnapshotEvent) => { + if (isActorRef(snapshotEvent.actorRef)) { + log( + '📸 %c%s%c updated due to %c%s%c:', + ...styleAsActor(snapshotEvent.actorRef.id), + ...styleAsKeyword(snapshotEvent.event.type), + snapshotEvent.snapshot + ); + } else { + log('📸 Snapshot due to %c%s%c:', ...styleAsKeyword(snapshotEvent.event.type), snapshotEvent); + } + }; + + return (inspectionEvent: InspectionEvent) => { + if (inspectionEvent.type === '@xstate.actor') { + logActorEvent(inspectionEvent); + } else if (inspectionEvent.type === '@xstate.event') { + logEventEvent(inspectionEvent); + } else if (inspectionEvent.type === '@xstate.snapshot') { + logSnapshotEvent(inspectionEvent); + } else { + log(`❓ Received inspection event:`, inspectionEvent); + } + }; +}; + +const isActorRef = (actorRefLike: ActorRefLike): actorRefLike is AnyActorRef => + 'id' in actorRefLike; + +const keywordStyle = 'font-weight: bold'; +const styleAsKeyword = (value: any) => [keywordStyle, value, ''] as const; + +const actorStyle = 'font-weight: bold; text-decoration: underline'; +const styleAsActor = (value: any) => [actorStyle, value, ''] as const; diff --git a/packages/kbn-xstate-utils/src/index.ts b/packages/kbn-xstate-utils/src/index.ts index 107585ba2096f..3edf83e8a32c2 100644 --- a/packages/kbn-xstate-utils/src/index.ts +++ b/packages/kbn-xstate-utils/src/index.ts @@ -9,5 +9,6 @@ export * from './actions'; export * from './dev_tools'; +export * from './console_inspector'; export * from './notification_channel'; export * from './types'; diff --git a/packages/serverless/settings/security_project/index.ts b/packages/serverless/settings/security_project/index.ts index 3932f924ea94d..dbbf6e506eda8 100644 --- a/packages/serverless/settings/security_project/index.ts +++ b/packages/serverless/settings/security_project/index.ts @@ -19,11 +19,9 @@ export const SECURITY_PROJECT_SETTINGS = [ settings.SECURITY_SOLUTION_DEFAULT_ANOMALY_SCORE_ID, settings.SECURITY_SOLUTION_RULES_TABLE_REFRESH_ID, settings.SECURITY_SOLUTION_IP_REPUTATION_LINKS_ID, - settings.SECURITY_SOLUTION_ENABLE_CCS_WARNING_ID, settings.SECURITY_SOLUTION_SHOW_RELATED_INTEGRATIONS_ID, settings.SECURITY_SOLUTION_NEWS_FEED_URL_ID, settings.SECURITY_SOLUTION_ENABLE_NEWS_FEED_ID, settings.SECURITY_SOLUTION_DEFAULT_ALERT_TAGS_KEY, settings.SECURITY_SOLUTION_ENABLE_ASSET_CRITICALITY_SETTING, - settings.SECURITY_SOLUTION_EXCLUDE_COLD_AND_FROZEN_TIERS_IN_ANALYZER, ]; diff --git a/packages/shared-ux/page/analytics_no_data/impl/src/analytics_no_data_page.component.tsx b/packages/shared-ux/page/analytics_no_data/impl/src/analytics_no_data_page.component.tsx index 41c525c5ca0b0..16d1bebd46548 100644 --- a/packages/shared-ux/page/analytics_no_data/impl/src/analytics_no_data_page.component.tsx +++ b/packages/shared-ux/page/analytics_no_data/impl/src/analytics_no_data_page.component.tsx @@ -22,12 +22,14 @@ import { getHasApiKeys$ } from '../lib/get_has_api_keys'; export interface Props { /** Handler for successfully creating a new data view. */ onDataViewCreated: (dataView: unknown) => void; - /** Handler for when try ES|QL is clicked and user has been navigated to try ES|QL in discover. */ - onESQLNavigationComplete?: () => void; /** if set to true allows creation of an ad-hoc dataview from data view editor */ allowAdHocDataView?: boolean; /** if the kibana instance is customly branded */ showPlainSpinner: boolean; + /** If the cluster has data, this handler allows the user to try ES|QL */ + onTryESQL?: () => void; + /** Handler for when try ES|QL is clicked and user has been navigated to try ES|QL in discover. */ + onESQLNavigationComplete?: () => void; } type AnalyticsNoDataPageProps = Props & @@ -119,9 +121,10 @@ const flavors: { */ export const AnalyticsNoDataPage: React.FC = ({ onDataViewCreated, - onESQLNavigationComplete, allowAdHocDataView, showPlainSpinner, + onTryESQL, + onESQLNavigationComplete, ...services }) => { const { prependBasePath, kibanaGuideDocLink, getHttp: get, pageFlavor } = services; @@ -138,8 +141,9 @@ export const AnalyticsNoDataPage: React.FC = ({ {...{ noDataConfig, onDataViewCreated, - onESQLNavigationComplete, allowAdHocDataView, + onTryESQL, + onESQLNavigationComplete, showPlainSpinner, }} /> diff --git a/packages/shared-ux/page/analytics_no_data/impl/src/analytics_no_data_page.stories.tsx b/packages/shared-ux/page/analytics_no_data/impl/src/analytics_no_data_page.stories.tsx index 3c75cefb38cb2..fa251cb03bdbe 100644 --- a/packages/shared-ux/page/analytics_no_data/impl/src/analytics_no_data_page.stories.tsx +++ b/packages/shared-ux/page/analytics_no_data/impl/src/analytics_no_data_page.stories.tsx @@ -29,8 +29,8 @@ export default { export const Analytics = (params: AnalyticsNoDataPageStorybookParams) => { return ( - - + + ); }; diff --git a/packages/shared-ux/page/analytics_no_data/impl/src/analytics_no_data_page.test.tsx b/packages/shared-ux/page/analytics_no_data/impl/src/analytics_no_data_page.test.tsx index 543c1c4817c5b..6b2d3441ed0d1 100644 --- a/packages/shared-ux/page/analytics_no_data/impl/src/analytics_no_data_page.test.tsx +++ b/packages/shared-ux/page/analytics_no_data/impl/src/analytics_no_data_page.test.tsx @@ -14,6 +14,7 @@ import { getAnalyticsNoDataPageServicesMock, getAnalyticsNoDataPageServicesMockWithCustomBranding, } from '@kbn/shared-ux-page-analytics-no-data-mocks'; +import { NoDataViewsPrompt } from '@kbn/shared-ux-prompt-no-data-views'; import { AnalyticsNoDataPageProvider } from './services'; import { AnalyticsNoDataPage as Component } from './analytics_no_data_page.component'; @@ -29,28 +30,86 @@ describe('AnalyticsNoDataPage', () => { jest.resetAllMocks(); }); - it('renders correctly', async () => { - const component = mountWithIntl( - - - - ); + describe('loading state', () => { + it('renders correctly', async () => { + const component = mountWithIntl( + + + + ); - await act(() => new Promise(setImmediate)); + await act(() => new Promise(setImmediate)); - expect(component.find(Component).length).toBe(1); - expect(component.find(Component).props().onDataViewCreated).toBe(onDataViewCreated); - expect(component.find(Component).props().allowAdHocDataView).toBe(true); + expect(component.find(Component).length).toBe(1); + expect(component.find(Component).props().onDataViewCreated).toBe(onDataViewCreated); + expect(component.find(Component).props().allowAdHocDataView).toBe(true); + }); + + it('passes correct boolean value to showPlainSpinner', async () => { + const component = mountWithIntl( + + + + ); + + await act(async () => { + component.update(); + }); + + expect(component.find(Component).length).toBe(1); + expect(component.find(Component).props().showPlainSpinner).toBe(true); + }); }); - it('passes correct boolean value to showPlainSpinner', () => { - const component = mountWithIntl( - - - - ); + describe('with ES data', () => { + jest.spyOn(services, 'hasESData').mockResolvedValue(true); + jest.spyOn(services, 'hasUserDataView').mockResolvedValue(false); + + it('renders the prompt to create a data view', async () => { + const onTryESQL = jest.fn(); + + await act(async () => { + const component = mountWithIntl( + + + + ); + + await new Promise(setImmediate); + component.update(); + + expect(component.find(Component).length).toBe(1); + expect(component.find(NoDataViewsPrompt).length).toBe(1); + }); + }); + + it('renders the prompt to create a data view with a custom onTryESQL action', async () => { + const onTryESQL = jest.fn(); + + await act(async () => { + const component = mountWithIntl( + + + + ); + + await new Promise(setImmediate); + component.update(); + + const tryESQLLink = component.find('button[data-test-subj="tryESQLLink"]'); + expect(tryESQLLink.length).toBe(1); + tryESQLLink.simulate('click'); - expect(component.find(Component).length).toBe(1); - expect(component.find(Component).props().showPlainSpinner).toBe(true); + expect(onTryESQL).toHaveBeenCalled(); + }); + }); }); }); diff --git a/packages/shared-ux/page/analytics_no_data/impl/src/analytics_no_data_page.tsx b/packages/shared-ux/page/analytics_no_data/impl/src/analytics_no_data_page.tsx index b64a296bbf74a..f7c80705daa58 100644 --- a/packages/shared-ux/page/analytics_no_data/impl/src/analytics_no_data_page.tsx +++ b/packages/shared-ux/page/analytics_no_data/impl/src/analytics_no_data_page.tsx @@ -20,8 +20,9 @@ import { AnalyticsNoDataPage as Component } from './analytics_no_data_page.compo */ export const AnalyticsNoDataPage = ({ onDataViewCreated, - onESQLNavigationComplete, allowAdHocDataView, + onTryESQL, + onESQLNavigationComplete, }: AnalyticsNoDataPageProps) => { const { customBranding, ...services } = useServices(); const showPlainSpinner = useObservable(customBranding.hasCustomBranding$) ?? false; @@ -33,6 +34,7 @@ export const AnalyticsNoDataPage = ({ allowAdHocDataView={allowAdHocDataView} onDataViewCreated={onDataViewCreated} onESQLNavigationComplete={onESQLNavigationComplete} + onTryESQL={onTryESQL} /> ); }; diff --git a/packages/shared-ux/page/analytics_no_data/impl/tsconfig.json b/packages/shared-ux/page/analytics_no_data/impl/tsconfig.json index 659aacfd3874d..ba872e1ecd761 100644 --- a/packages/shared-ux/page/analytics_no_data/impl/tsconfig.json +++ b/packages/shared-ux/page/analytics_no_data/impl/tsconfig.json @@ -23,6 +23,7 @@ "@kbn/i18n-react", "@kbn/core-http-browser", "@kbn/core-http-browser-mocks", + "@kbn/shared-ux-prompt-no-data-views", ], "exclude": [ "target/**/*", diff --git a/packages/shared-ux/page/analytics_no_data/mocks/src/storybook.ts b/packages/shared-ux/page/analytics_no_data/mocks/src/storybook.ts index c664bb192518c..f8cca693a072c 100644 --- a/packages/shared-ux/page/analytics_no_data/mocks/src/storybook.ts +++ b/packages/shared-ux/page/analytics_no_data/mocks/src/storybook.ts @@ -18,9 +18,14 @@ import type { } from '@kbn/shared-ux-page-analytics-no-data-types'; import { of } from 'rxjs'; +interface PropArguments { + useCustomOnTryESQL: boolean; +} + type ServiceArguments = Pick; -export type Params = ArgumentParams<{}, ServiceArguments> & KibanaNoDataPageStorybookParams; +export type Params = ArgumentParams & + KibanaNoDataPageStorybookParams; const kibanaNoDataMock = new KibanaNoDataPageStorybookMock(); @@ -30,7 +35,13 @@ export class StorybookMock extends AbstractStorybookMock< {}, ServiceArguments > { - propArguments = {}; + propArguments = { + // requires hasESData to be toggled to true + useCustomOnTryESQL: { + control: 'boolean', + defaultValue: false, + }, + }; serviceArguments = { kibanaGuideDocLink: { control: 'text', @@ -59,9 +70,10 @@ export class StorybookMock extends AbstractStorybookMock< }; } - getProps() { + getProps(params: Params) { return { onDataViewCreated: action('onDataViewCreated'), + onTryESQL: params.useCustomOnTryESQL ? action('onTryESQL-from-props') : undefined, }; } } diff --git a/packages/shared-ux/page/analytics_no_data/types/index.d.ts b/packages/shared-ux/page/analytics_no_data/types/index.d.ts index 9fd6653a48b6a..94bf85500da6b 100644 --- a/packages/shared-ux/page/analytics_no_data/types/index.d.ts +++ b/packages/shared-ux/page/analytics_no_data/types/index.d.ts @@ -70,6 +70,8 @@ export interface AnalyticsNoDataPageProps { onDataViewCreated: (dataView: unknown) => void; /** if set to true allows creation of an ad-hoc data view from data view editor */ allowAdHocDataView?: boolean; + /** If the cluster has data, this handler allows the user to try ES|QL */ + onTryESQL?: () => void; /** Handler for when try ES|QL is clicked and user has been navigated to try ES|QL in discover. */ onESQLNavigationComplete?: () => void; } diff --git a/packages/shared-ux/page/kibana_no_data/impl/src/kibana_no_data_page.tsx b/packages/shared-ux/page/kibana_no_data/impl/src/kibana_no_data_page.tsx index 2042d7fa1420d..d74c3aabd5662 100644 --- a/packages/shared-ux/page/kibana_no_data/impl/src/kibana_no_data_page.tsx +++ b/packages/shared-ux/page/kibana_no_data/impl/src/kibana_no_data_page.tsx @@ -20,9 +20,10 @@ import { useServices } from './services'; */ export const KibanaNoDataPage = ({ onDataViewCreated, - onESQLNavigationComplete, noDataConfig, allowAdHocDataView, + onTryESQL, + onESQLNavigationComplete, showPlainSpinner, }: KibanaNoDataPageProps) => { // These hooks are temporary, until this component is moved to a package. @@ -58,8 +59,9 @@ export const KibanaNoDataPage = ({ return ( ); } diff --git a/packages/shared-ux/page/kibana_no_data/types/index.d.ts b/packages/shared-ux/page/kibana_no_data/types/index.d.ts index 56067e9d555f9..c391149f7efaa 100644 --- a/packages/shared-ux/page/kibana_no_data/types/index.d.ts +++ b/packages/shared-ux/page/kibana_no_data/types/index.d.ts @@ -60,6 +60,8 @@ export interface KibanaNoDataPageProps { allowAdHocDataView?: boolean; /** Set to true if the kibana is customly branded */ showPlainSpinner: boolean; + /** If the cluster has data, this handler allows the user to try ES|QL */ + onTryESQL?: () => void; /** Handler for when try ES|QL is clicked and user has been navigated to try ES|QL in discover. */ onESQLNavigationComplete?: () => void; } diff --git a/packages/shared-ux/prompt/no_data_views/impl/src/actions.tsx b/packages/shared-ux/prompt/no_data_views/impl/src/actions.tsx deleted file mode 100644 index 6f2af97df6e04..0000000000000 --- a/packages/shared-ux/prompt/no_data_views/impl/src/actions.tsx +++ /dev/null @@ -1,76 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the "Elastic License - * 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side - * Public License v 1"; you may not use this file except in compliance with, at - * your election, the "Elastic License 2.0", the "GNU Affero General Public - * License v3.0 only", or the "Server Side Public License, v 1". - */ - -import { EuiButton, EuiLink, EuiSpacer, EuiText } from '@elastic/eui'; -import { i18n } from '@kbn/i18n'; -import { FormattedMessage } from '@kbn/i18n-react'; -import React from 'react'; - -interface NoDataButtonProps { - onClickCreate: (() => void) | undefined; - canCreateNewDataView: boolean; - onTryESQL?: () => void; - esqlDocLink?: string; -} - -const createDataViewText = i18n.translate('sharedUXPackages.noDataViewsPrompt.addDataViewText', { - defaultMessage: 'Create data view', -}); - -export const NoDataButtonLink = ({ - onClickCreate, - canCreateNewDataView, - onTryESQL, - esqlDocLink, -}: NoDataButtonProps) => { - if (!onTryESQL && !canCreateNewDataView) { - return null; - } - - return ( - <> - {canCreateNewDataView && ( - - {createDataViewText} - - )} - {canCreateNewDataView && onTryESQL && } - {onTryESQL && ( - - - - - ), - }} - /> - - - - - - )} - - ); -}; diff --git a/packages/shared-ux/prompt/no_data_views/impl/src/data_view_illustration.tsx b/packages/shared-ux/prompt/no_data_views/impl/src/data_view_illustration.tsx index cb817225254a9..099cdc87a21eb 100644 --- a/packages/shared-ux/prompt/no_data_views/impl/src/data_view_illustration.tsx +++ b/packages/shared-ux/prompt/no_data_views/impl/src/data_view_illustration.tsx @@ -26,5 +26,14 @@ export const DataViewIllustration = () => { } `; - return Data view illustration; + return ( + Data view illustration + ); }; diff --git a/packages/shared-ux/prompt/no_data_views/impl/src/documentation_link.tsx b/packages/shared-ux/prompt/no_data_views/impl/src/documentation_link.tsx index 8e74bead6922e..d190764af947d 100644 --- a/packages/shared-ux/prompt/no_data_views/impl/src/documentation_link.tsx +++ b/packages/shared-ux/prompt/no_data_views/impl/src/documentation_link.tsx @@ -13,9 +13,10 @@ import { FormattedMessage } from '@kbn/i18n-react'; interface Props { href: string; + ['data-test-subj']?: string; } -export function DocumentationLink({ href }: Props) { +export function DocumentationLink({ href, ['data-test-subj']: dataTestSubj }: Props) { return (
@@ -28,7 +29,7 @@ export function DocumentationLink({ href }: Props) {
- + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/packages/kbn-ace/src/ace/modes/x_json/worker/index.ts b/packages/shared-ux/prompt/no_data_views/impl/src/esql_illustration.tsx similarity index 64% rename from packages/kbn-ace/src/ace/modes/x_json/worker/index.ts rename to packages/shared-ux/prompt/no_data_views/impl/src/esql_illustration.tsx index b09099ed9ad01..a2da4c416ed55 100644 --- a/packages/kbn-ace/src/ace/modes/x_json/worker/index.ts +++ b/packages/shared-ux/prompt/no_data_views/impl/src/esql_illustration.tsx @@ -7,10 +7,18 @@ * License v3.0 only", or the "Server Side Public License, v 1". */ -// @ts-ignore -import src from '!!raw-loader!./x_json.ace.worker'; +import React from 'react'; -export const workerModule = { - id: 'ace/mode/json_worker', - src, +import png from './esql_illustration.svg'; + +export const EsqlIllustration = () => { + return ( + ES|QL illustration + ); }; diff --git a/packages/shared-ux/prompt/no_data_views/impl/src/no_data_views.component.test.tsx b/packages/shared-ux/prompt/no_data_views/impl/src/no_data_views.component.test.tsx index ad2e176a511f0..75363c80b67b5 100644 --- a/packages/shared-ux/prompt/no_data_views/impl/src/no_data_views.component.test.tsx +++ b/packages/shared-ux/prompt/no_data_views/impl/src/no_data_views.component.test.tsx @@ -9,7 +9,7 @@ import React from 'react'; import { mountWithIntl } from '@kbn/test-jest-helpers'; -import { EuiButton, EuiEmptyPrompt } from '@elastic/eui'; +import { EuiButton, EuiCard } from '@elastic/eui'; import { NoDataViewsPrompt } from './no_data_views.component'; import { DocumentationLink } from './documentation_link'; @@ -19,36 +19,64 @@ describe('', () => { ); - expect(component.find(EuiEmptyPrompt).length).toBe(1); - expect(component.find(EuiButton).length).toBe(1); - expect(component.find(DocumentationLink).length).toBe(1); + expect(component.find(EuiCard).length).toBe(2); + expect(component.find(EuiButton).length).toBe(2); + expect(component.find(DocumentationLink).length).toBe(2); + + expect(component.find('EuiButton[data-test-subj="createDataViewButton"]').length).toBe(1); + expect(component.find('DocumentationLink[data-test-subj="docLinkDataViews"]').length).toBe(1); + + expect(component.find('EuiButton[data-test-subj="tryESQLLink"]').length).toBe(1); + expect(component.find('DocumentationLink[data-test-subj="docLinkEsql"]').length).toBe(1); }); - test('does not render button if canCreateNewDataViews is false', () => { + test('does not render "Create data view" button if canCreateNewDataViews is false', () => { const component = mountWithIntl(); - expect(component.find(EuiButton).length).toBe(0); + expect(component.find('EuiButton[data-test-subj="createDataViewButton"]').length).toBe(0); }); - test('does not documentation link if linkToDocumentation is not provided', () => { + test('does not render documentation links if links to documentation are not provided', () => { const component = mountWithIntl( ); - expect(component.find(DocumentationLink).length).toBe(0); + expect(component.find('DocumentationLink[data-test-subj="docLinkDataViews"]').length).toBe(0); + expect(component.find('DocumentationLink[data-test-subj="docLinkEsql"]').length).toBe(0); }); test('onClickCreate', () => { const onClickCreate = jest.fn(); const component = mountWithIntl( - + ); - component.find('button').simulate('click'); + component.find('button[data-test-subj="createDataViewButton"]').simulate('click'); expect(onClickCreate).toHaveBeenCalledTimes(1); }); + + test('onClickTryEsql', () => { + const onClickTryEsql = jest.fn(); + const component = mountWithIntl( + + ); + + component.find('button[data-test-subj="tryESQLLink"]').simulate('click'); + + expect(onClickTryEsql).toHaveBeenCalledTimes(1); + }); }); diff --git a/packages/shared-ux/prompt/no_data_views/impl/src/no_data_views.component.tsx b/packages/shared-ux/prompt/no_data_views/impl/src/no_data_views.component.tsx index d5807891e734d..3bfed37aa0b1a 100644 --- a/packages/shared-ux/prompt/no_data_views/impl/src/no_data_views.component.tsx +++ b/packages/shared-ux/prompt/no_data_views/impl/src/no_data_views.component.tsx @@ -7,95 +7,222 @@ * License v3.0 only", or the "Server Side Public License, v 1". */ -import React from 'react'; import { css } from '@emotion/react'; +import React from 'react'; -import { EuiEmptyPrompt, EuiPanel } from '@elastic/eui'; +import { + EuiButton, + EuiCard, + EuiFlexGroup, + EuiFlexItem, + EuiHorizontalRule, + EuiSpacer, + EuiText, + EuiTextAlign, + EuiToolTip, + useEuiPaddingCSS, +} from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n-react'; -import { withSuspense } from '@kbn/shared-ux-utility'; import { NoDataViewsPromptComponentProps } from '@kbn/shared-ux-prompt-no-data-views-types'; import { DocumentationLink } from './documentation_link'; -import { NoDataButtonLink } from './actions'; +import { DataViewIllustration } from './data_view_illustration'; +import { EsqlIllustration } from './esql_illustration'; -// Using raw value because it is content dependent -const MAX_WIDTH = 830; +// max width value to use in pixels +const MAX_WIDTH = 770; -/** - * A presentational component that is shown in cases when there are no data views created yet. - */ -export const NoDataViewsPrompt = ({ +const PromptAddDataViews = ({ onClickCreate, canCreateNewDataView, dataViewsDocLink, + emptyPromptColor, +}: Pick< + NoDataViewsPromptComponentProps, + 'onClickCreate' | 'canCreateNewDataView' | 'dataViewsDocLink' | 'emptyPromptColor' +>) => { + const icon = ; + + const title = ( + + ); + + const description = ( + <> + {canCreateNewDataView ? ( + + ) : ( + + )} + + ); + + const footer = dataViewsDocLink ? ( + <> + {canCreateNewDataView ? ( + + + + ) : ( + + } + > + + + + + )} + + + + ) : undefined; + + return ( + + ); +}; + +const PromptTryEsql = ({ onTryESQL, esqlDocLink, - emptyPromptColor = 'plain', -}: NoDataViewsPromptComponentProps) => { - const title = canCreateNewDataView ? ( -

- -
- -

- ) : ( -

- -

- ); + emptyPromptColor, +}: Pick< + NoDataViewsPromptComponentProps, + 'onClickCreate' | 'onTryESQL' | 'esqlDocLink' | 'emptyPromptColor' +>) => { + if (!onTryESQL) { + // we need to handle the case where the Try ES|QL click handler is not set because + // onTryESQL is set via a useEffect that has asynchronous dependencies + return null; + } + + const icon = ; - const body = canCreateNewDataView ? ( -

- -

- ) : ( -

- -

+ const title = ( + ); - const footer = dataViewsDocLink ? : undefined; + const description = ( + + ); - // Load this illustration lazily - const Illustration = withSuspense( - React.lazy(() => - import('./data_view_illustration').then(({ DataViewIllustration }) => { - return { default: DataViewIllustration }; - }) - ), - + const footer = ( + <> + + + + + {esqlDocLink && } + ); - const icon = ; - const actions = ( - + return ( + ); +}; + +/** + * A presentational component that is shown in cases when there are no data views created yet. + */ +export const NoDataViewsPrompt = ({ + onClickCreate, + canCreateNewDataView, + dataViewsDocLink, + onTryESQL, + esqlDocLink, + emptyPromptColor = 'plain', +}: NoDataViewsPromptComponentProps) => { + const cssStyles = [ + css` + max-width: ${MAX_WIDTH}px; + `, + useEuiPaddingCSS('top').m, + useEuiPaddingCSS('right').m, + useEuiPaddingCSS('left').m, + ]; return ( - + > + + + +

+ +

+
+
+ + + + + + + + + + + +
+ ); }; diff --git a/packages/shared-ux/prompt/no_data_views/impl/src/no_data_views.tsx b/packages/shared-ux/prompt/no_data_views/impl/src/no_data_views.tsx index 43ae5f267ea90..340147505cb25 100644 --- a/packages/shared-ux/prompt/no_data_views/impl/src/no_data_views.tsx +++ b/packages/shared-ux/prompt/no_data_views/impl/src/no_data_views.tsx @@ -27,12 +27,15 @@ type CloseDataViewEditorFn = ReturnType { - const { canCreateNewDataView, openDataViewEditor, dataViewsDocLink, onTryESQL, esqlDocLink } = + const { canCreateNewDataView, openDataViewEditor, dataViewsDocLink, esqlDocLink, ...services } = useServices(); + const onTryESQL = onTryESQLProp ?? services.onTryESQL; + const closeDataViewEditor = useRef(); useEffect(() => { diff --git a/packages/shared-ux/prompt/no_data_views/impl/tsconfig.json b/packages/shared-ux/prompt/no_data_views/impl/tsconfig.json index 673823e620474..2af357080c07c 100644 --- a/packages/shared-ux/prompt/no_data_views/impl/tsconfig.json +++ b/packages/shared-ux/prompt/no_data_views/impl/tsconfig.json @@ -16,8 +16,6 @@ ], "kbn_references": [ "@kbn/i18n-react", - "@kbn/i18n", - "@kbn/shared-ux-utility", "@kbn/test-jest-helpers", "@kbn/shared-ux-prompt-no-data-views-types", "@kbn/shared-ux-prompt-no-data-views-mocks", diff --git a/packages/shared-ux/prompt/no_data_views/mocks/src/storybook.ts b/packages/shared-ux/prompt/no_data_views/mocks/src/storybook.ts index 63f46d2008077..973152201587d 100644 --- a/packages/shared-ux/prompt/no_data_views/mocks/src/storybook.ts +++ b/packages/shared-ux/prompt/no_data_views/mocks/src/storybook.ts @@ -34,17 +34,19 @@ export class StorybookMock extends AbstractStorybookMock< defaultValue: true, }, dataViewsDocLink: { - options: ['some/link', undefined], - control: { type: 'radio' }, - }, - esqlDocLink: { - options: ['some/link', undefined], + options: ['dataviews/link', undefined], control: { type: 'radio' }, + defaultValue: 'dataviews/link', }, canTryEsql: { control: 'boolean', defaultValue: true, }, + esqlDocLink: { + options: ['esql/link', undefined], + control: { type: 'radio' }, + defaultValue: 'esql/link', + }, }; dependencies = []; @@ -59,7 +61,7 @@ export class StorybookMock extends AbstractStorybookMock< let onTryESQL; if (canTryEsql !== false) { - onTryESQL = action('onTryESQL'); + onTryESQL = action('onTryESQL-from-services'); } return { diff --git a/packages/shared-ux/prompt/no_data_views/types/index.d.ts b/packages/shared-ux/prompt/no_data_views/types/index.d.ts index 15f9f53c59fe6..7bca285bee717 100644 --- a/packages/shared-ux/prompt/no_data_views/types/index.d.ts +++ b/packages/shared-ux/prompt/no_data_views/types/index.d.ts @@ -42,7 +42,7 @@ export interface NoDataViewsPromptServices { openDataViewEditor: (options: DataViewEditorOptions) => () => void; /** A link to information about Data Views in Kibana */ dataViewsDocLink: string; - /** Get a handler for trying ES|QL */ + /** If the cluster has data, this handler allows the user to try ES|QL */ onTryESQL: (() => void) | undefined; /** A link to the documentation for ES|QL */ esqlDocLink: string; @@ -92,7 +92,7 @@ export interface NoDataViewsPromptComponentProps { emptyPromptColor?: EuiEmptyPromptProps['color']; /** Click handler for create button. **/ onClickCreate?: () => void; - /** Handler for someone wanting to try ES|QL. */ + /** If the cluster has data, this handler allows the user to try ES|QL */ onTryESQL?: () => void; /** Link to documentation on ES|QL. */ esqlDocLink?: string; @@ -104,6 +104,10 @@ export interface NoDataViewsPromptProps { allowAdHocDataView?: boolean; /** Handler for successfully creating a new data view. */ onDataViewCreated: (dataView: unknown) => void; + /** If the cluster has data, this handler allows the user to try ES|QL */ + onTryESQL?: () => void; /** Handler for when try ES|QL is clicked and user has been navigated to try ES|QL in discover. */ onESQLNavigationComplete?: () => void; + /** Empty prompt color **/ + emptyPromptColor?: PanelColor; } diff --git a/renovate.json b/renovate.json index dccc37ef702a4..ff7ee4b0aaafa 100644 --- a/renovate.json +++ b/renovate.json @@ -58,7 +58,7 @@ "matchDepNames": ["@elastic/charts"], "reviewers": ["team:visualizations", "markov00", "nickofthyme"], "matchBaseBranches": ["main"], - "labels": ["release_note:skip", "backport:skip", "Team:Visualizations"], + "labels": ["release_note:skip", "backport:prev-minor", "Team:Visualizations"], "enabled": true }, { diff --git a/src/core/public/styles/_ace_overrides.scss b/src/core/public/styles/_ace_overrides.scss deleted file mode 100644 index ca5230b46acd3..0000000000000 --- a/src/core/public/styles/_ace_overrides.scss +++ /dev/null @@ -1,202 +0,0 @@ -// SASSTODO: Replace with an EUI editor -// Intentionally not using the EuiCodeBlock colors here because they actually change -// hue from light to dark theme. So some colors would change while others wouldn't. -// Seemed weird, so just hexing all the colors but using the `makeHighContrastColor()` -// function to ensure accessible contrast. - -// In order to override the TM (Textmate) theme of Ace/Brace, everywhere, -// it is being scoped by a known outer selector -.kbnBody { - .ace-tm { - $aceBackground: tintOrShade($euiColorLightShade, 50%, 0); - - background-color: $euiColorLightestShade; - color: $euiTextColor; - - .ace_scrollbar { - @include euiScrollBar; - } - - .ace_gutter-active-line, - .ace_marker-layer .ace_active-line { - background-color: transparentize($euiColorLightShade, .3); - } - - .ace_snippet-marker { - width: 100%; - background-color: $aceBackground; - border: none; - } - - .ace_indent-guide { - background: linear-gradient(to left, $euiColorMediumShade 0%, $euiColorMediumShade 1px, transparent 1px, transparent 100%); - } - - .ace_search { - z-index: $euiZLevel1 + 1; - } - - .ace_layer.ace_marker-layer { - overflow: visible; - } - - .ace_warning { - color: $euiColorDanger; - } - - .ace_method { - color: makeHighContrastColor(#DD0A73, $aceBackground); - } - - .ace_url, - .ace_start_triple_quote, - .ace_end_triple_quote { - color: makeHighContrastColor(#00A69B, $aceBackground); - } - - .ace_multi_string { - color: makeHighContrastColor(#009926, $aceBackground); - font-style: italic; - } - - .ace_gutter { - background-color: $euiColorEmptyShade; - color: $euiColorDarkShade; - border-left: 1px solid $aceBackground; - } - - .ace_print-margin { - width: 1px; - background: $euiColorLightShade; - } - - .ace_fold { - background-color: #6B72E6; - } - - .ace_cursor { - color: $euiColorFullShade; - } - - .ace_invisible { - color: $euiColorLightShade; - } - - .ace_storage, - .ace_keyword { - color: makeHighContrastColor(#0079A5, $aceBackground); - } - - .ace_constant { - color: makeHighContrastColor(#900, $aceBackground); - } - - .ace_constant.ace_buildin { - color: makeHighContrastColor(rgb(88, 72, 246), $aceBackground); - } - - .ace_constant.ace_language { - color: makeHighContrastColor(rgb(88, 92, 246), $aceBackground); - } - - .ace_constant.ace_library { - color: makeHighContrastColor(#009926, $aceBackground); - } - - .ace_invalid { - background-color: euiCallOutColor('danger', 'background'); - color: euiCallOutColor('danger', 'foreground'); - } - - .ace_support.ace_function { - color: makeHighContrastColor(rgb(60, 76, 114), $aceBackground); - } - - .ace_support.ace_constant { - color: makeHighContrastColor(#009926, $aceBackground); - } - - .ace_support.ace_type, - .ace_support.ace_class { - color: makeHighContrastColor(rgb(109, 121, 222), $aceBackground); - } - - .ace_keyword.ace_operator { - color: makeHighContrastColor($euiColorDarkShade, $aceBackground); - } - - .ace_string { - color: makeHighContrastColor(#009926, $aceBackground); - } - - .ace_comment { - color: makeHighContrastColor(rgb(76, 136, 107), $aceBackground); - } - - .ace_comment.ace_doc { - color: makeHighContrastColor(#0079A5, $aceBackground); - } - - .ace_comment.ace_doc.ace_tag { - color: makeHighContrastColor($euiColorMediumShade, $aceBackground); - } - - .ace_constant.ace_numeric { - color: makeHighContrastColor(#0079A5, $aceBackground); - } - - .ace_variable { - color: makeHighContrastColor(#0079A5, $aceBackground); - } - - .ace_xml-pe { - color: makeHighContrastColor($euiColorDarkShade, $aceBackground); - } - - .ace_entity.ace_name.ace_function { - color: makeHighContrastColor(#0000A2, $aceBackground); - } - - .ace_heading { - color: makeHighContrastColor(rgb(12, 7, 255), $aceBackground); - } - - .ace_list { - color: makeHighContrastColor(rgb(185, 6, 144), $aceBackground); - } - - .ace_meta.ace_tag { - color: makeHighContrastColor(rgb(0, 22, 142), $aceBackground); - } - - .ace_string.ace_regex { - color: makeHighContrastColor(rgb(255, 0, 0), $aceBackground); - } - - .ace_marker-layer .ace_selection { - background: tintOrShade($euiColorPrimary, 70%, 70%); - } - - &.ace_multiselect .ace_selection.ace_start { - box-shadow: 0 0 3px 0 $euiColorEmptyShade; - } - - .ace_marker-layer .ace_step { - background: tintOrShade($euiColorWarning, 80%, 80%); - } - - .ace_marker-layer .ace_stack { - background: tintOrShade($euiColorSuccess, 80%, 80%); - } - - .ace_marker-layer .ace_bracket { - margin: -1px 0 0 -1px; - border: $euiBorderThin; - } - - .ace_marker-layer .ace_selected-word { - background: $euiColorLightestShade; - border: $euiBorderThin; - } - } -} diff --git a/src/core/public/styles/_index.scss b/src/core/public/styles/_index.scss index 42981c7e07398..cfdb1c7192dcd 100644 --- a/src/core/public/styles/_index.scss +++ b/src/core/public/styles/_index.scss @@ -1,4 +1,3 @@ @import './base'; -@import './ace_overrides'; @import './chrome/index'; @import './rendering/index'; diff --git a/src/core/server/index.ts b/src/core/server/index.ts index f4852bdc97fe3..1ac38b1d44157 100644 --- a/src/core/server/index.ts +++ b/src/core/server/index.ts @@ -267,7 +267,6 @@ export { PluginType } from '@kbn/core-base-common'; export type { PrebootPlugin, Plugin, - AsyncPlugin, PluginConfigDescriptor, PluginConfigSchema, PluginInitializer, diff --git a/src/core/server/integration_tests/ci_checks/saved_objects/check_registered_types.test.ts b/src/core/server/integration_tests/ci_checks/saved_objects/check_registered_types.test.ts index c8803a1fbd071..7736e1ad7e90b 100644 --- a/src/core/server/integration_tests/ci_checks/saved_objects/check_registered_types.test.ts +++ b/src/core/server/integration_tests/ci_checks/saved_objects/check_registered_types.test.ts @@ -91,7 +91,7 @@ describe('checking migration metadata changes on all registered SO types', () => "endpoint:unified-user-artifact-manifest": "71c7fcb52c658b21ea2800a6b6a76972ae1c776e", "endpoint:user-artifact-manifest": "1c3533161811a58772e30cdc77bac4631da3ef2b", "enterprise_search_telemetry": "9ac912e1417fc8681e0cd383775382117c9e3d3d", - "entity-definition": "61be3e95966045122b55e181bb39658b1dc9bbe9", + "entity-definition": "e3811fd5fbb878d170067c0d6897a2e63010af36", "entity-discovery-api-key": "c267a65c69171d1804362155c1378365f5acef88", "entity-engine-status": "0738aa1a06d3361911740f8f166071ea43a00927", "epm-packages": "8042d4a1522f6c4e6f5486e791b3ffe3a22f88fd", diff --git a/src/core/server/integration_tests/http/oas.test.ts b/src/core/server/integration_tests/http/oas.test.ts index c6a1d4e308356..413b8b01754b5 100644 --- a/src/core/server/integration_tests/http/oas.test.ts +++ b/src/core/server/integration_tests/http/oas.test.ts @@ -193,7 +193,9 @@ it('only accepts "public" or "internal" for "access" query param', async () => { const server = await startService({ config: { server: { oas: { enabled: true } } } }); const result = await supertest(server.listener).get('/api/oas').query({ access: 'invalid' }); expect(result.body.message).toBe( - 'Invalid access query parameter. Must be one of "public" or "internal".' + `[access]: types that failed validation: +- [access.0]: expected value to equal [public] +- [access.1]: expected value to equal [internal]` ); expect(result.status).toBe(400); }); diff --git a/src/core/server/integration_tests/http/router.test.ts b/src/core/server/integration_tests/http/router.test.ts index 0b7bbb8ce55c3..c0a690e479e67 100644 --- a/src/core/server/integration_tests/http/router.test.ts +++ b/src/core/server/integration_tests/http/router.test.ts @@ -836,6 +836,82 @@ describe('Handler', () => { expect(body).toEqual(12); }); + + it('adds versioned header v2023-10-31 to public, unversioned routes', async () => { + const { server: innerServer, createRouter } = await server.setup(setupDeps); + const router = createRouter('/'); + + router.post( + { + path: '/public', + validate: { body: schema.object({ ok: schema.boolean() }) }, + options: { + access: 'public', + }, + }, + (context, req, res) => { + if (req.body.ok) { + return res.ok({ body: 'ok', headers: { test: 'this' } }); + } + return res.customError({ statusCode: 499, body: 'custom error' }); + } + ); + router.post( + { + path: '/internal', + validate: { body: schema.object({ ok: schema.boolean() }) }, + }, + (context, req, res) => { + return res.ok({ body: 'ok', headers: { test: 'this' } }); + } + ); + await server.start(); + + // Includes header if validation fails + { + const { headers } = await supertest(innerServer.listener) + .post('/public') + .send({ ok: null }) + .expect(400); + expect(headers).toMatchObject({ 'elastic-api-version': '2023-10-31' }); + } + + // Includes header if custom error + { + const { headers } = await supertest(innerServer.listener) + .post('/public') + .send({ ok: false }) + .expect(499); + expect(headers).toMatchObject({ 'elastic-api-version': '2023-10-31' }); + } + + // Includes header if OK + { + const { headers } = await supertest(innerServer.listener) + .post('/public') + .send({ ok: true }) + .expect(200); + expect(headers).toMatchObject({ 'elastic-api-version': '2023-10-31' }); + } + + // Internal unversioned routes do not include the header for OK + { + const { headers } = await supertest(innerServer.listener) + .post('/internal') + .send({ ok: true }) + .expect(200); + expect(headers).not.toMatchObject({ 'elastic-api-version': '2023-10-31' }); + } + + // Internal unversioned routes do not include the header for validation failures + { + const { headers } = await supertest(innerServer.listener) + .post('/internal') + .send({ ok: null }) + .expect(400); + expect(headers).not.toMatchObject({ 'elastic-api-version': '2023-10-31' }); + } + }); }); describe('handleLegacyErrors', () => { diff --git a/src/core/server/integration_tests/http/versioned_router.test.ts b/src/core/server/integration_tests/http/versioned_router.test.ts index 9f2b2625a6a7e..254337f82abcf 100644 --- a/src/core/server/integration_tests/http/versioned_router.test.ts +++ b/src/core/server/integration_tests/http/versioned_router.test.ts @@ -112,14 +112,12 @@ describe('Routing versioned requests', () => { await server.start(); - await expect(supertest.get('/my-path').expect(200)).resolves.toEqual( - expect.objectContaining({ - body: { v: '1' }, - header: expect.objectContaining({ - 'elastic-api-version': '2020-02-02', - }), - }) - ); + await expect(supertest.get('/my-path').expect(200)).resolves.toMatchObject({ + body: { v: '1' }, + header: expect.objectContaining({ + 'elastic-api-version': '2020-02-02', + }), + }); }); it('returns the expected output for badly formatted versions', async () => { @@ -137,11 +135,9 @@ describe('Routing versioned requests', () => { .set('Elastic-Api-Version', 'abc') .expect(400) .then(({ body }) => body) - ).resolves.toEqual( - expect.objectContaining({ - message: expect.stringMatching(/Invalid version/), - }) - ); + ).resolves.toMatchObject({ + message: expect.stringMatching(/Invalid version/), + }); }); it('returns the expected responses for failed validation', async () => { @@ -163,18 +159,14 @@ describe('Routing versioned requests', () => { await server.start(); await expect( - supertest - .post('/my-path') - .send({}) - .set('Elastic-Api-Version', '1') - .expect(400) - .then(({ body }) => body) - ).resolves.toEqual( - expect.objectContaining({ + supertest.post('/my-path').send({}).set('Elastic-Api-Version', '1').expect(400) + ).resolves.toMatchObject({ + body: { error: 'Bad Request', message: expect.stringMatching(/expected value of type/), - }) - ); + }, + headers: { 'elastic-api-version': '1' }, // includes version if validation failed + }); expect(captureErrorMock).not.toHaveBeenCalled(); }); @@ -193,7 +185,7 @@ describe('Routing versioned requests', () => { .set('Elastic-Api-Version', '2023-10-31') .expect(200) .then(({ header }) => header) - ).resolves.toEqual(expect.objectContaining({ 'elastic-api-version': '2023-10-31' })); + ).resolves.toMatchObject({ 'elastic-api-version': '2023-10-31' }); }); it('runs response validation when in dev', async () => { @@ -236,11 +228,9 @@ describe('Routing versioned requests', () => { .set('Elastic-Api-Version', '1') .expect(500) .then(({ body }) => body) - ).resolves.toEqual( - expect.objectContaining({ - message: expect.stringMatching(/Failed output validation/), - }) - ); + ).resolves.toMatchObject({ + message: expect.stringMatching(/Failed output validation/), + }); await expect( supertest @@ -248,11 +238,9 @@ describe('Routing versioned requests', () => { .set('Elastic-Api-Version', '2') .expect(500) .then(({ body }) => body) - ).resolves.toEqual( - expect.objectContaining({ - message: expect.stringMatching(/Failed output validation/), - }) - ); + ).resolves.toMatchObject({ + message: expect.stringMatching(/Failed output validation/), + }); // This should pass response validation await expect( @@ -261,11 +249,9 @@ describe('Routing versioned requests', () => { .set('Elastic-Api-Version', '3') .expect(200) .then(({ body }) => body) - ).resolves.toEqual( - expect.objectContaining({ - v: '3', - }) - ); + ).resolves.toMatchObject({ + v: '3', + }); expect(captureErrorMock).not.toHaveBeenCalled(); }); @@ -367,9 +353,7 @@ describe('Routing versioned requests', () => { .set('Elastic-Api-Version', '2020-02-02') .expect(500) .then(({ body }) => body) - ).resolves.toEqual( - expect.objectContaining({ message: expect.stringMatching(/No handlers registered/) }) - ); + ).resolves.toMatchObject({ message: expect.stringMatching(/No handlers registered/) }); expect(captureErrorMock).not.toHaveBeenCalled(); }); diff --git a/src/dev/precommit_hook/casing_check_config.js b/src/dev/precommit_hook/casing_check_config.js index 2eaeb64f8be5f..3572781c4b262 100644 --- a/src/dev/precommit_hook/casing_check_config.js +++ b/src/dev/precommit_hook/casing_check_config.js @@ -87,6 +87,9 @@ export const IGNORE_FILE_GLOBS = [ // Support for including http-client.env.json configurations '**/http-client.env.json', + + // updatecli configuration for driving the UBI/Ironbank image updates + 'updatecli-compose.yaml', ]; /** diff --git a/src/plugins/bfetch/server/ui_settings.ts b/src/plugins/bfetch/server/ui_settings.ts index aee4903d226c0..132dd19ef8b9c 100644 --- a/src/plugins/bfetch/server/ui_settings.ts +++ b/src/plugins/bfetch/server/ui_settings.ts @@ -18,7 +18,7 @@ export function getUiSettings(): Record> { name: i18n.translate('bfetch.disableBfetch', { defaultMessage: 'Disable request batching', }), - value: false, + value: true, description: i18n.translate('bfetch.disableBfetchDesc', { defaultMessage: 'Disables requests batching. This increases number of HTTP requests from Kibana, but allows to debug requests individually.', diff --git a/src/plugins/console/README.md b/src/plugins/console/README.md index 02da27229286a..35921de334380 100644 --- a/src/plugins/console/README.md +++ b/src/plugins/console/README.md @@ -44,7 +44,7 @@ POST /_some_endpoint ``` ## Architecture -Console uses Ace editor that is wrapped with [`CoreEditor`](https://github.com/elastic/kibana/blob/main/src/plugins/console/public/types/core_editor.ts), so that if needed it can easily be replaced with another editor, for example Monaco. +Console uses Monaco editor that is wrapped with [`kbn-monaco`](https://github.com/elastic/kibana/blob/main/packages/kbn-monaco/index.ts), so that if needed it can easily be replaced with another editor. The autocomplete logic is located in [`autocomplete`](https://github.com/elastic/kibana/blob/main/src/plugins/console/public/lib/autocomplete) folder. Autocomplete rules are computed by classes in `components` sub-folder. ## Autocomplete definitions @@ -317,8 +317,4 @@ Another change is replacing jQuery with the core http client to communicate with ### Outstanding issues #### Autocomplete suggestions for Kibana API endpoints Console currently supports autocomplete suggestions for Elasticsearch API endpoints. The autocomplete suggestions for Kibana API endpoints are not supported yet. -Related issue: [#130661](https://github.com/elastic/kibana/issues/130661) - -#### Migration to Monaco Editor -Console plugin is currently using Ace Editor and it is planned to migrate to Monaco Editor in the future. -Related issue: [#57435](https://github.com/elastic/kibana/issues/57435) \ No newline at end of file +Related issue: [#130661](https://github.com/elastic/kibana/issues/130661) \ No newline at end of file diff --git a/src/plugins/console/public/application/containers/editor/utils/autocomplete_utils.test.ts b/src/plugins/console/public/application/containers/editor/utils/autocomplete_utils.test.ts index 0dc35062d015d..f7c6bd3b32054 100644 --- a/src/plugins/console/public/application/containers/editor/utils/autocomplete_utils.test.ts +++ b/src/plugins/console/public/application/containers/editor/utils/autocomplete_utils.test.ts @@ -160,6 +160,10 @@ describe('autocomplete_utils', () => { name: 'index2', meta: 'index', }, + { + name: '.index', + meta: 'index', + }, ] as AutoCompleteContext['autoCompleteSet']; // mock the populateContext function that finds the correct autocomplete endpoint object and puts it into the context object mockPopulateContext.mockImplementation((...args) => { @@ -189,7 +193,7 @@ describe('autocomplete_utils', () => { expect(items.every((item) => item.detail === 'index')).toBe(true); }); - it('suggest endpoints and index names if no comma', () => { + it('suggest endpoints and index names, excluding dot-prefixed ones, if no comma and no dot', () => { const mockModel = { getValueInRange: () => 'GET _search', getWordUntilPosition: () => ({ startColumn: 12 }), @@ -197,6 +201,19 @@ describe('autocomplete_utils', () => { const mockPosition = { lineNumber: 1, column: 12 } as unknown as monaco.Position; const items = getUrlPathCompletionItems(mockModel, mockPosition); expect(items.length).toBe(4); + expect( + items.every((item) => typeof item.label === 'string' && item.label.startsWith('.')) + ).toBe(false); + }); + + it('suggests all endpoints and indices, including dot-prefixed ones, if last char is a dot', () => { + const mockModel = { + getValueInRange: () => 'GET .', + getWordUntilPosition: () => ({ startColumn: 6 }), + } as unknown as monaco.editor.ITextModel; + const mockPosition = { lineNumber: 1, column: 6 } as unknown as monaco.Position; + const items = getUrlPathCompletionItems(mockModel, mockPosition); + expect(items.length).toBe(5); }); }); }); diff --git a/src/plugins/console/public/application/containers/editor/utils/autocomplete_utils.ts b/src/plugins/console/public/application/containers/editor/utils/autocomplete_utils.ts index cfe0341c39780..c36542b43e75e 100644 --- a/src/plugins/console/public/application/containers/editor/utils/autocomplete_utils.ts +++ b/src/plugins/console/public/application/containers/editor/utils/autocomplete_utils.ts @@ -154,6 +154,12 @@ export const getUrlPathCompletionItems = ( }; return ( filterTermsWithoutName(autoCompleteSet) + .filter( + (term) => + // Only keep dot-prefixed terms if the user typed in a dot + !(typeof term.name === 'string' && term.name.startsWith('.')) || + lineContent.trim().endsWith('.') + ) // map autocomplete items to completion items .map((item) => { return { diff --git a/src/plugins/console/public/application/contexts/editor_context/editor_registry.ts b/src/plugins/console/public/application/contexts/editor_context/editor_registry.ts index 8197ff0460e86..dc7b58ecbd267 100644 --- a/src/plugins/console/public/application/contexts/editor_context/editor_registry.ts +++ b/src/plugins/console/public/application/contexts/editor_context/editor_registry.ts @@ -8,12 +8,11 @@ */ import { MonacoEditorActionsProvider } from '../../containers/editor/monaco_editor_actions_provider'; -import { SenseEditor } from '../../models/sense_editor'; export class EditorRegistry { - private inputEditor: SenseEditor | MonacoEditorActionsProvider | undefined; + private inputEditor: MonacoEditorActionsProvider | undefined; - setInputEditor(inputEditor: SenseEditor | MonacoEditorActionsProvider) { + setInputEditor(inputEditor: MonacoEditorActionsProvider) { this.inputEditor = inputEditor; } diff --git a/src/plugins/console/public/application/hooks/index.ts b/src/plugins/console/public/application/hooks/index.ts index b6b7211a940e4..29c554771dad0 100644 --- a/src/plugins/console/public/application/hooks/index.ts +++ b/src/plugins/console/public/application/hooks/index.ts @@ -8,7 +8,6 @@ */ export { useSetInputEditor } from './use_set_input_editor'; -export { useRestoreRequestFromHistory } from './use_restore_request_from_history'; -export { useSendCurrentRequest, sendRequest } from './use_send_current_request'; +export { sendRequest } from './use_send_current_request'; export { useSaveCurrentTextObject } from './use_save_current_text_object'; export { useDataInit } from './use_data_init'; diff --git a/src/plugins/console/public/application/hooks/use_restore_request_from_history/index.ts b/src/plugins/console/public/application/hooks/use_restore_request_from_history/index.ts deleted file mode 100644 index 47f12868d9bc6..0000000000000 --- a/src/plugins/console/public/application/hooks/use_restore_request_from_history/index.ts +++ /dev/null @@ -1,10 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the "Elastic License - * 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side - * Public License v 1"; you may not use this file except in compliance with, at - * your election, the "Elastic License 2.0", the "GNU Affero General Public - * License v3.0 only", or the "Server Side Public License, v 1". - */ - -export { useRestoreRequestFromHistory } from './use_restore_request_from_history'; diff --git a/src/plugins/console/public/application/hooks/use_restore_request_from_history/restore_request_from_history.ts b/src/plugins/console/public/application/hooks/use_restore_request_from_history/restore_request_from_history.ts deleted file mode 100644 index 897e499dc481e..0000000000000 --- a/src/plugins/console/public/application/hooks/use_restore_request_from_history/restore_request_from_history.ts +++ /dev/null @@ -1,48 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the "Elastic License - * 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side - * Public License v 1"; you may not use this file except in compliance with, at - * your election, the "Elastic License 2.0", the "GNU Affero General Public - * License v3.0 only", or the "Server Side Public License, v 1". - */ - -import RowParser from '../../../lib/row_parser'; -import { ESRequest } from '../../../types'; -import { SenseEditor } from '../../models/sense_editor'; -import { formatRequestBodyDoc } from '../../../lib/utils'; - -export function restoreRequestFromHistory(editor: SenseEditor, req: ESRequest) { - const coreEditor = editor.getCoreEditor(); - let pos = coreEditor.getCurrentPosition(); - let prefix = ''; - let suffix = '\n'; - const parser = new RowParser(coreEditor); - if (parser.isStartRequestRow(pos.lineNumber)) { - pos.column = 1; - suffix += '\n'; - } else if (parser.isEndRequestRow(pos.lineNumber)) { - const line = coreEditor.getLineValue(pos.lineNumber); - pos.column = line.length + 1; - prefix = '\n\n'; - } else if (parser.isInBetweenRequestsRow(pos.lineNumber)) { - pos.column = 1; - } else { - pos = editor.nextRequestEnd(pos); - prefix = '\n\n'; - } - - let s = prefix + req.method + ' ' + req.endpoint; - if (req.data) { - const indent = true; - const formattedData = formatRequestBodyDoc([req.data], indent); - s += '\n' + formattedData.data; - } - - s += suffix; - - coreEditor.insert(pos, s); - coreEditor.moveCursorToPosition({ lineNumber: pos.lineNumber + prefix.length, column: 1 }); - coreEditor.clearSelection(); - coreEditor.getContainer().focus(); -} diff --git a/src/plugins/console/public/application/hooks/use_restore_request_from_history/restore_request_from_history_to_monaco.ts b/src/plugins/console/public/application/hooks/use_restore_request_from_history/restore_request_from_history_to_monaco.ts deleted file mode 100644 index 08c2bc6af86a3..0000000000000 --- a/src/plugins/console/public/application/hooks/use_restore_request_from_history/restore_request_from_history_to_monaco.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", the "GNU Affero General Public License v3.0 only", and the "Server Side - * Public License v 1"; you may not use this file except in compliance with, at - * your election, the "Elastic License 2.0", the "GNU Affero General Public - * License v3.0 only", or the "Server Side Public License, v 1". - */ - -import { formatRequestBodyDoc } from '../../../lib/utils'; -import { MonacoEditorActionsProvider } from '../../containers/editor/monaco_editor_actions_provider'; -import { ESRequest } from '../../../types'; - -export async function restoreRequestFromHistoryToMonaco( - provider: MonacoEditorActionsProvider, - req: ESRequest -) { - let s = req.method + ' ' + req.endpoint; - if (req.data) { - const indent = true; - const formattedData = formatRequestBodyDoc([req.data], indent); - s += '\n' + formattedData.data; - } - await provider.restoreRequestFromHistory(s); -} diff --git a/src/plugins/console/public/application/hooks/use_restore_request_from_history/use_restore_request_from_history.ts b/src/plugins/console/public/application/hooks/use_restore_request_from_history/use_restore_request_from_history.ts deleted file mode 100644 index 5ee0d185923c2..0000000000000 --- a/src/plugins/console/public/application/hooks/use_restore_request_from_history/use_restore_request_from_history.ts +++ /dev/null @@ -1,21 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the "Elastic License - * 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side - * Public License v 1"; you may not use this file except in compliance with, at - * your election, the "Elastic License 2.0", the "GNU Affero General Public - * License v3.0 only", or the "Server Side Public License, v 1". - */ - -import { useCallback } from 'react'; -import { instance as registry } from '../../contexts/editor_context/editor_registry'; -import { ESRequest } from '../../../types'; -import { restoreRequestFromHistoryToMonaco } from './restore_request_from_history_to_monaco'; -import { MonacoEditorActionsProvider } from '../../containers/editor/monaco_editor_actions_provider'; - -export const useRestoreRequestFromHistory = () => { - return useCallback(async (req: ESRequest) => { - const editor = registry.getInputEditor(); - await restoreRequestFromHistoryToMonaco(editor as MonacoEditorActionsProvider, req); - }, []); -}; diff --git a/src/plugins/console/public/application/hooks/use_send_current_request/index.ts b/src/plugins/console/public/application/hooks/use_send_current_request/index.ts index 753184f67e998..656c0b939cf5b 100644 --- a/src/plugins/console/public/application/hooks/use_send_current_request/index.ts +++ b/src/plugins/console/public/application/hooks/use_send_current_request/index.ts @@ -7,5 +7,4 @@ * License v3.0 only", or the "Server Side Public License, v 1". */ -export { useSendCurrentRequest } from './use_send_current_request'; export { sendRequest } from './send_request'; diff --git a/src/plugins/console/public/application/hooks/use_send_current_request/track.ts b/src/plugins/console/public/application/hooks/use_send_current_request/track.ts deleted file mode 100644 index e663c0b8354c1..0000000000000 --- a/src/plugins/console/public/application/hooks/use_send_current_request/track.ts +++ /dev/null @@ -1,34 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the "Elastic License - * 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side - * Public License v 1"; you may not use this file except in compliance with, at - * your election, the "Elastic License 2.0", the "GNU Affero General Public - * License v3.0 only", or the "Server Side Public License, v 1". - */ - -import { SenseEditor } from '../../models/sense_editor'; -import { getEndpointFromPosition } from '../../../lib/autocomplete/get_endpoint_from_position'; -import { MetricsTracker } from '../../../types'; - -export const track = ( - requests: Array<{ method: string }>, - editor: SenseEditor, - trackUiMetric: MetricsTracker -) => { - const coreEditor = editor.getCoreEditor(); - // `getEndpointFromPosition` gets values from the server-side generated JSON files which - // are a combination of JS, automatically generated JSON and manual overrides. That means - // the metrics reported from here will be tied to the definitions in those files. - // See src/legacy/core_plugins/console/server/api_server/spec - const endpointDescription = getEndpointFromPosition( - coreEditor, - coreEditor.getCurrentPosition(), - editor.parser - ); - - if (requests[0] && endpointDescription) { - const eventName = `${requests[0].method}_${endpointDescription.id ?? 'unknown'}`; - trackUiMetric.count(eventName); - } -}; diff --git a/src/plugins/console/public/application/hooks/use_send_current_request/use_send_current_request.test.tsx b/src/plugins/console/public/application/hooks/use_send_current_request/use_send_current_request.test.tsx deleted file mode 100644 index 7f3082d5ef3dc..0000000000000 --- a/src/plugins/console/public/application/hooks/use_send_current_request/use_send_current_request.test.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", the "GNU Affero General Public License v3.0 only", and the "Server Side - * Public License v 1"; you may not use this file except in compliance with, at - * your election, the "Elastic License 2.0", the "GNU Affero General Public - * License v3.0 only", or the "Server Side Public License, v 1". - */ - -jest.mock('./send_request', () => ({ sendRequest: jest.fn() })); -jest.mock('../../contexts/editor_context/editor_registry', () => ({ - instance: { getInputEditor: jest.fn() }, -})); -jest.mock('./track', () => ({ track: jest.fn() })); -jest.mock('../../contexts/request_context', () => ({ useRequestActionContext: jest.fn() })); -jest.mock('../../../lib/utils', () => ({ replaceVariables: jest.fn() })); - -import React from 'react'; -import { renderHook, act } from '@testing-library/react-hooks'; - -import { ContextValue, ServicesContextProvider } from '../../contexts'; -import { serviceContextMock } from '../../contexts/services_context.mock'; -import { useRequestActionContext } from '../../contexts/request_context'; -import { instance as editorRegistry } from '../../contexts/editor_context/editor_registry'; -import * as utils from '../../../lib/utils'; - -import { sendRequest } from './send_request'; -import { useSendCurrentRequest } from './use_send_current_request'; - -describe('useSendCurrentRequest', () => { - let mockContextValue: ContextValue; - let dispatch: (...args: unknown[]) => void; - const contexts = ({ children }: { children: JSX.Element }) => ( - {children} - ); - - beforeEach(() => { - mockContextValue = serviceContextMock.create(); - dispatch = jest.fn(); - (useRequestActionContext as jest.Mock).mockReturnValue(dispatch); - (utils.replaceVariables as jest.Mock).mockReturnValue(['test']); - }); - - afterEach(() => { - jest.resetAllMocks(); - }); - - it('calls send request', async () => { - // Set up mocks - (mockContextValue.services.settings.toJSON as jest.Mock).mockReturnValue({}); - // This request should succeed - (sendRequest as jest.Mock).mockResolvedValue([]); - (editorRegistry.getInputEditor as jest.Mock).mockImplementation(() => ({ - getRequestsInRange: () => ['test'], - })); - - const { result } = renderHook(() => useSendCurrentRequest(), { wrapper: contexts }); - await act(() => result.current()); - expect(sendRequest).toHaveBeenCalledWith({ - http: mockContextValue.services.http, - requests: ['test'], - }); - - // Second call should be the request success - const [, [requestSucceededCall]] = (dispatch as jest.Mock).mock.calls; - expect(requestSucceededCall).toEqual({ type: 'requestSuccess', payload: { data: [] } }); - }); - - it('handles known errors', async () => { - // Set up mocks - (sendRequest as jest.Mock).mockRejectedValue({ response: 'nada' }); - (editorRegistry.getInputEditor as jest.Mock).mockImplementation(() => ({ - getRequestsInRange: () => ['test'], - })); - - const { result } = renderHook(() => useSendCurrentRequest(), { wrapper: contexts }); - await act(() => result.current()); - // Second call should be the request failure - const [, [requestFailedCall]] = (dispatch as jest.Mock).mock.calls; - - // The request must have concluded - expect(requestFailedCall).toEqual({ type: 'requestFail', payload: { response: 'nada' } }); - }); - - it('handles unknown errors', async () => { - // Set up mocks - (sendRequest as jest.Mock).mockRejectedValue(NaN /* unexpected error value */); - (editorRegistry.getInputEditor as jest.Mock).mockImplementation(() => ({ - getRequestsInRange: () => ['test'], - })); - - const { result } = renderHook(() => useSendCurrentRequest(), { wrapper: contexts }); - await act(() => result.current()); - // Second call should be the request failure - const [, [requestFailedCall]] = (dispatch as jest.Mock).mock.calls; - - // The request must have concluded - expect(requestFailedCall).toEqual({ type: 'requestFail', payload: undefined }); - // It also notified the user - expect(mockContextValue.services.notifications.toasts.addError).toHaveBeenCalledWith(NaN, { - title: 'Unknown Request Error', - }); - }); - - it('notifies the user about save to history errors once only', async () => { - // Set up mocks - (sendRequest as jest.Mock).mockReturnValue( - [{ request: {} }, { request: {} }] /* two responses to save history */ - ); - (mockContextValue.services.settings.toJSON as jest.Mock).mockReturnValue({ - isHistoryEnabled: true, - }); - (mockContextValue.services.history.addToHistory as jest.Mock).mockImplementation(() => { - // Mock throwing - throw new Error('cannot save!'); - }); - (editorRegistry.getInputEditor as jest.Mock).mockImplementation(() => ({ - getRequestsInRange: () => ['test', 'test'], - })); - - const { result } = renderHook(() => useSendCurrentRequest(), { wrapper: contexts }); - await act(() => result.current()); - - expect(dispatch).toHaveBeenCalledTimes(2); - - expect(mockContextValue.services.history.addToHistory).toHaveBeenCalledTimes(2); - // It only called notification once - expect(mockContextValue.services.notifications.toasts.addError).toHaveBeenCalledTimes(1); - }); -}); diff --git a/src/plugins/console/public/application/hooks/use_send_current_request/use_send_current_request.ts b/src/plugins/console/public/application/hooks/use_send_current_request/use_send_current_request.ts deleted file mode 100644 index afdd5358432e9..0000000000000 --- a/src/plugins/console/public/application/hooks/use_send_current_request/use_send_current_request.ts +++ /dev/null @@ -1,148 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the "Elastic License - * 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side - * Public License v 1"; you may not use this file except in compliance with, at - * your election, the "Elastic License 2.0", the "GNU Affero General Public - * License v3.0 only", or the "Server Side Public License, v 1". - */ - -import { i18n } from '@kbn/i18n'; -import { useCallback } from 'react'; - -import { toMountPoint } from '../../../shared_imports'; -import { isQuotaExceededError } from '../../../services/history'; -import { instance as registry } from '../../contexts/editor_context/editor_registry'; -import { useRequestActionContext, useServicesContext } from '../../contexts'; -import { StorageQuotaError } from '../../components/storage_quota_error'; -import { sendRequest } from './send_request'; -import { track } from './track'; -import { replaceVariables } from '../../../lib/utils'; -import { StorageKeys } from '../../../services'; -import { DEFAULT_VARIABLES } from '../../../../common/constants'; -import { SenseEditor } from '../../models'; - -export const useSendCurrentRequest = () => { - const { - services: { history, settings, notifications, trackUiMetric, http, autocompleteInfo, storage }, - ...startServices - } = useServicesContext(); - - const dispatch = useRequestActionContext(); - - return useCallback(async () => { - try { - const editor = registry.getInputEditor() as SenseEditor; - const variables = storage.get(StorageKeys.VARIABLES, DEFAULT_VARIABLES); - let requests = await editor.getRequestsInRange(); - requests = replaceVariables(requests, variables); - if (!requests.length) { - notifications.toasts.add( - i18n.translate('console.notification.error.noRequestSelectedTitle', { - defaultMessage: - 'No request selected. Select a request by placing the cursor inside it.', - }) - ); - return; - } - - dispatch({ type: 'sendRequest', payload: undefined }); - - // Fire and forget - setTimeout(() => track(requests, editor as SenseEditor, trackUiMetric), 0); - - const results = await sendRequest({ http, requests }); - - let saveToHistoryError: undefined | Error; - const { isHistoryEnabled } = settings.toJSON(); - - if (isHistoryEnabled) { - results.forEach(({ request: { path, method, data } }) => { - try { - history.addToHistory(path, method, data); - } catch (e) { - // Grab only the first error - if (!saveToHistoryError) { - saveToHistoryError = e; - } - } - }); - } - - if (saveToHistoryError) { - const errorTitle = i18n.translate('console.notification.error.couldNotSaveRequestTitle', { - defaultMessage: 'Could not save request to Console history.', - }); - if (isQuotaExceededError(saveToHistoryError)) { - const toast = notifications.toasts.addWarning({ - title: i18n.translate('console.notification.error.historyQuotaReachedMessage', { - defaultMessage: - 'Request history is full. Clear the console history or disable saving new requests.', - }), - text: toMountPoint( - StorageQuotaError({ - onClearHistory: () => { - history.clearHistory(); - notifications.toasts.remove(toast); - }, - onDisableSavingToHistory: () => { - settings.setIsHistoryEnabled(false); - notifications.toasts.remove(toast); - }, - }), - startServices - ), - }); - } else { - // Best effort, but still notify the user. - notifications.toasts.addError(saveToHistoryError, { - title: errorTitle, - }); - } - } - - const { polling } = settings.toJSON(); - if (polling) { - // If the user has submitted a request against ES, something in the fields, indices, aliases, - // or templates may have changed, so we'll need to update this data. Assume that if - // the user disables polling they're trying to optimize performance or otherwise - // preserve resources, so they won't want this request sent either. - autocompleteInfo.retrieve(settings, settings.getAutocomplete()); - } - - dispatch({ - type: 'requestSuccess', - payload: { - data: results, - }, - }); - } catch (e) { - if (e?.response) { - dispatch({ - type: 'requestFail', - payload: e, - }); - } else { - dispatch({ - type: 'requestFail', - payload: undefined, - }); - notifications.toasts.addError(e, { - title: i18n.translate('console.notification.error.unknownErrorTitle', { - defaultMessage: 'Unknown Request Error', - }), - }); - } - } - }, [ - storage, - dispatch, - http, - settings, - notifications.toasts, - trackUiMetric, - history, - autocompleteInfo, - startServices, - ]); -}; diff --git a/src/plugins/console/public/application/hooks/use_set_input_editor.ts b/src/plugins/console/public/application/hooks/use_set_input_editor.ts index d6029420a1772..148ede97520ea 100644 --- a/src/plugins/console/public/application/hooks/use_set_input_editor.ts +++ b/src/plugins/console/public/application/hooks/use_set_input_editor.ts @@ -10,14 +10,13 @@ import { useCallback } from 'react'; import { useEditorActionContext } from '../contexts/editor_context'; import { instance as registry } from '../contexts/editor_context/editor_registry'; -import { SenseEditor } from '../models'; import { MonacoEditorActionsProvider } from '../containers/editor/monaco_editor_actions_provider'; export const useSetInputEditor = () => { const dispatch = useEditorActionContext(); return useCallback( - (editor: SenseEditor | MonacoEditorActionsProvider) => { + (editor: MonacoEditorActionsProvider) => { dispatch({ type: 'setInputEditor', payload: editor }); registry.setInputEditor(editor); }, diff --git a/src/plugins/console/public/application/models/legacy_core_editor/create.ts b/src/plugins/console/public/application/models/legacy_core_editor/create.ts deleted file mode 100644 index b2631e8d6712b..0000000000000 --- a/src/plugins/console/public/application/models/legacy_core_editor/create.ts +++ /dev/null @@ -1,20 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the "Elastic License - * 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side - * Public License v 1"; you may not use this file except in compliance with, at - * your election, the "Elastic License 2.0", the "GNU Affero General Public - * License v3.0 only", or the "Server Side Public License, v 1". - */ - -import ace from 'brace'; -import { LegacyCoreEditor } from './legacy_core_editor'; - -export const create = (el: HTMLElement) => { - const actions = document.querySelector('#ConAppEditorActions'); - if (!actions) { - throw new Error('Could not find ConAppEditorActions element!'); - } - const aceEditor = ace.edit(el); - return new LegacyCoreEditor(aceEditor, actions); -}; diff --git a/src/plugins/console/public/application/models/legacy_core_editor/create_readonly.ts b/src/plugins/console/public/application/models/legacy_core_editor/create_readonly.ts deleted file mode 100644 index dc0a95c224395..0000000000000 --- a/src/plugins/console/public/application/models/legacy_core_editor/create_readonly.ts +++ /dev/null @@ -1,81 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the "Elastic License - * 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side - * Public License v 1"; you may not use this file except in compliance with, at - * your election, the "Elastic License 2.0", the "GNU Affero General Public - * License v3.0 only", or the "Server Side Public License, v 1". - */ - -import _ from 'lodash'; -import ace from 'brace'; -import { Mode } from './mode/output'; -import smartResize from './smart_resize'; - -export interface CustomAceEditor extends ace.Editor { - update: (text: string, mode?: string | Mode, cb?: () => void) => void; - append: (text: string, foldPrevious?: boolean, cb?: () => void) => void; -} - -/** - * Note: using read-only ace editor leaks the Ace editor API - use this as sparingly as possible or - * create an interface for it so that we don't rely directly on vendor APIs. - */ -export function createReadOnlyAceEditor(element: HTMLElement): CustomAceEditor { - const output: CustomAceEditor = ace.acequire('ace/ace').edit(element); - - const outputMode = new Mode(); - - output.$blockScrolling = Infinity; - output.resize = smartResize(output); - output.update = (val, mode, cb) => { - if (typeof mode === 'function') { - cb = mode as () => void; - mode = void 0; - } - - const session = output.getSession(); - const currentMode = val ? mode || outputMode : 'ace/mode/text'; - - // @ts-ignore - // ignore ts error here due to type definition mistake in brace for setMode(mode: string): void; - // this method accepts string or SyntaxMode which is an object. See https://github.com/ajaxorg/ace/blob/13dc911dbc0ea31ca343d5744b3f472767458fc3/ace.d.ts#L467 - session.setMode(currentMode); - session.setValue(val); - if (typeof cb === 'function') { - setTimeout(cb); - } - }; - - output.append = (val: string, foldPrevious?: boolean, cb?: () => void) => { - if (typeof foldPrevious === 'function') { - cb = foldPrevious; - foldPrevious = true; - } - if (_.isUndefined(foldPrevious)) { - foldPrevious = true; - } - const session = output.getSession(); - const lastLine = session.getLength(); - if (foldPrevious) { - output.moveCursorTo(Math.max(0, lastLine - 1), 0); - } - session.insert({ row: lastLine, column: 0 }, '\n' + val); - output.moveCursorTo(lastLine + 1, 0); - if (typeof cb === 'function') { - setTimeout(cb); - } - }; - - (function setupSession(session) { - session.setMode('ace/mode/text'); - (session as unknown as { setFoldStyle: (v: string) => void }).setFoldStyle('markbeginend'); - session.setTabSize(2); - session.setUseWrapMode(true); - })(output.getSession()); - - output.setShowPrintMargin(false); - output.setReadOnly(true); - - return output; -} diff --git a/src/plugins/console/public/application/models/legacy_core_editor/index.ts b/src/plugins/console/public/application/models/legacy_core_editor/index.ts deleted file mode 100644 index e885257520245..0000000000000 --- a/src/plugins/console/public/application/models/legacy_core_editor/index.ts +++ /dev/null @@ -1,18 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the "Elastic License - * 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side - * Public License v 1"; you may not use this file except in compliance with, at - * your election, the "Elastic License 2.0", the "GNU Affero General Public - * License v3.0 only", or the "Server Side Public License, v 1". - */ - -import 'brace'; -import 'brace/ext/language_tools'; -import 'brace/ext/searchbox'; -import 'brace/mode/json'; -import 'brace/mode/text'; - -export * from './legacy_core_editor'; -export * from './create_readonly'; -export * from './create'; diff --git a/src/plugins/console/public/application/models/legacy_core_editor/input.test.js b/src/plugins/console/public/application/models/legacy_core_editor/input.test.js deleted file mode 100644 index e472edc1af125..0000000000000 --- a/src/plugins/console/public/application/models/legacy_core_editor/input.test.js +++ /dev/null @@ -1,559 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the "Elastic License - * 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side - * Public License v 1"; you may not use this file except in compliance with, at - * your election, the "Elastic License 2.0", the "GNU Affero General Public - * License v3.0 only", or the "Server Side Public License, v 1". - */ - -import './legacy_core_editor.test.mocks'; -import RowParser from '../../../lib/row_parser'; -import { createTokenIterator } from '../../factories'; -import $ from 'jquery'; -import { create } from './create'; - -describe('Input', () => { - let coreEditor; - beforeEach(() => { - // Set up our document body - document.body.innerHTML = `
-
-
-
-
`; - - coreEditor = create(document.querySelector('#ConAppEditor')); - - $(coreEditor.getContainer()).show(); - }); - afterEach(() => { - $(coreEditor.getContainer()).hide(); - }); - - describe('.getLineCount', () => { - it('returns the correct line length', async () => { - await coreEditor.setValue('1\n2\n3\n4', true); - expect(coreEditor.getLineCount()).toBe(4); - }); - }); - - describe('Tokenization', () => { - function tokensAsList() { - const iter = createTokenIterator({ - editor: coreEditor, - position: { lineNumber: 1, column: 1 }, - }); - const ret = []; - let t = iter.getCurrentToken(); - const parser = new RowParser(coreEditor); - if (parser.isEmptyToken(t)) { - t = parser.nextNonEmptyToken(iter); - } - while (t) { - ret.push({ value: t.value, type: t.type }); - t = parser.nextNonEmptyToken(iter); - } - - return ret; - } - - let testCount = 0; - - function tokenTest(tokenList, prefix, data) { - if (data && typeof data !== 'string') { - data = JSON.stringify(data, null, 3); - } - if (data) { - if (prefix) { - data = prefix + '\n' + data; - } - } else { - data = prefix; - } - - test('Token test ' + testCount++ + ' prefix: ' + prefix, async function () { - await coreEditor.setValue(data, true); - const tokens = tokensAsList(); - const normTokenList = []; - for (let i = 0; i < tokenList.length; i++) { - normTokenList.push({ type: tokenList[i++], value: tokenList[i] }); - } - - expect(tokens).toEqual(normTokenList); - }); - } - - tokenTest(['method', 'GET', 'url.part', '_search'], 'GET _search'); - - tokenTest(['method', 'GET', 'url.slash', '/', 'url.part', '_search'], 'GET /_search'); - - tokenTest( - [ - 'method', - 'GET', - 'url.protocol_host', - 'http://somehost', - 'url.slash', - '/', - 'url.part', - '_search', - ], - 'GET http://somehost/_search' - ); - - tokenTest(['method', 'GET', 'url.protocol_host', 'http://somehost'], 'GET http://somehost'); - - tokenTest( - ['method', 'GET', 'url.protocol_host', 'http://somehost', 'url.slash', '/'], - 'GET http://somehost/' - ); - - tokenTest( - ['method', 'GET', 'url.protocol_host', 'http://test:user@somehost', 'url.slash', '/'], - 'GET http://test:user@somehost/' - ); - - tokenTest( - ['method', 'GET', 'url.part', '_cluster', 'url.slash', '/', 'url.part', 'nodes'], - 'GET _cluster/nodes' - ); - - tokenTest( - [ - 'method', - 'GET', - 'url.slash', - '/', - 'url.part', - '_cluster', - 'url.slash', - '/', - 'url.part', - 'nodes', - ], - 'GET /_cluster/nodes' - ); - - tokenTest( - ['method', 'GET', 'url.part', 'index', 'url.slash', '/', 'url.part', '_search'], - 'GET index/_search' - ); - - tokenTest(['method', 'GET', 'url.part', 'index'], 'GET index'); - - tokenTest( - ['method', 'GET', 'url.part', 'index', 'url.slash', '/', 'url.part', 'type'], - 'GET index/type' - ); - - tokenTest( - [ - 'method', - 'GET', - 'url.slash', - '/', - 'url.part', - 'index', - 'url.slash', - '/', - 'url.part', - 'type', - 'url.slash', - '/', - ], - 'GET /index/type/' - ); - - tokenTest( - [ - 'method', - 'GET', - 'url.part', - 'index', - 'url.slash', - '/', - 'url.part', - 'type', - 'url.slash', - '/', - 'url.part', - '_search', - ], - 'GET index/type/_search' - ); - - tokenTest( - [ - 'method', - 'GET', - 'url.part', - 'index', - 'url.slash', - '/', - 'url.part', - 'type', - 'url.slash', - '/', - 'url.part', - '_search', - 'url.questionmark', - '?', - 'url.param', - 'value', - 'url.equal', - '=', - 'url.value', - '1', - ], - 'GET index/type/_search?value=1' - ); - - tokenTest( - [ - 'method', - 'GET', - 'url.part', - 'index', - 'url.slash', - '/', - 'url.part', - 'type', - 'url.slash', - '/', - 'url.part', - '1', - ], - 'GET index/type/1' - ); - - tokenTest( - [ - 'method', - 'GET', - 'url.slash', - '/', - 'url.part', - 'index1', - 'url.comma', - ',', - 'url.part', - 'index2', - 'url.slash', - '/', - ], - 'GET /index1,index2/' - ); - - tokenTest( - [ - 'method', - 'GET', - 'url.slash', - '/', - 'url.part', - 'index1', - 'url.comma', - ',', - 'url.part', - 'index2', - 'url.slash', - '/', - 'url.part', - '_search', - ], - 'GET /index1,index2/_search' - ); - - tokenTest( - [ - 'method', - 'GET', - 'url.part', - 'index1', - 'url.comma', - ',', - 'url.part', - 'index2', - 'url.slash', - '/', - 'url.part', - '_search', - ], - 'GET index1,index2/_search' - ); - - tokenTest( - [ - 'method', - 'GET', - 'url.slash', - '/', - 'url.part', - 'index1', - 'url.comma', - ',', - 'url.part', - 'index2', - ], - 'GET /index1,index2' - ); - - tokenTest( - ['method', 'GET', 'url.part', 'index1', 'url.comma', ',', 'url.part', 'index2'], - 'GET index1,index2' - ); - - tokenTest( - ['method', 'GET', 'url.slash', '/', 'url.part', 'index1', 'url.comma', ','], - 'GET /index1,' - ); - - tokenTest( - ['method', 'PUT', 'url.slash', '/', 'url.part', 'index', 'url.slash', '/'], - 'PUT /index/' - ); - - tokenTest( - ['method', 'GET', 'url.part', 'index', 'url.slash', '/', 'url.part', '_search'], - 'GET index/_search ' - ); - - tokenTest(['method', 'PUT', 'url.slash', '/', 'url.part', 'index'], 'PUT /index'); - - tokenTest( - [ - 'method', - 'PUT', - 'url.slash', - '/', - 'url.part', - 'index1', - 'url.comma', - ',', - 'url.part', - 'index2', - 'url.slash', - '/', - 'url.part', - 'type1', - 'url.comma', - ',', - 'url.part', - 'type2', - ], - 'PUT /index1,index2/type1,type2' - ); - - tokenTest( - [ - 'method', - 'PUT', - 'url.slash', - '/', - 'url.part', - 'index1', - 'url.slash', - '/', - 'url.part', - 'type1', - 'url.comma', - ',', - 'url.part', - 'type2', - 'url.comma', - ',', - ], - 'PUT /index1/type1,type2,' - ); - - tokenTest( - [ - 'method', - 'PUT', - 'url.part', - 'index1', - 'url.comma', - ',', - 'url.part', - 'index2', - 'url.slash', - '/', - 'url.part', - 'type1', - 'url.comma', - ',', - 'url.part', - 'type2', - 'url.slash', - '/', - 'url.part', - '1234', - ], - 'PUT index1,index2/type1,type2/1234' - ); - - tokenTest( - [ - 'method', - 'POST', - 'url.part', - '_search', - 'paren.lparen', - '{', - 'variable', - '"q"', - 'punctuation.colon', - ':', - 'paren.lparen', - '{', - 'paren.rparen', - '}', - 'paren.rparen', - '}', - ], - 'POST _search\n' + '{\n' + ' "q": {}\n' + ' \n' + '}' - ); - - tokenTest( - [ - 'method', - 'POST', - 'url.part', - '_search', - 'paren.lparen', - '{', - 'variable', - '"q"', - 'punctuation.colon', - ':', - 'paren.lparen', - '{', - 'variable', - '"s"', - 'punctuation.colon', - ':', - 'paren.lparen', - '{', - 'paren.rparen', - '}', - 'paren.rparen', - '}', - 'paren.rparen', - '}', - ], - 'POST _search\n' + '{\n' + ' "q": { "s": {}}\n' + ' \n' + '}' - ); - - function statesAsList() { - const ret = []; - const maxLine = coreEditor.getLineCount(); - for (let line = 1; line <= maxLine; line++) ret.push(coreEditor.getLineState(line)); - return ret; - } - - function statesTest(statesList, prefix, data) { - if (data && typeof data !== 'string') { - data = JSON.stringify(data, null, 3); - } - if (data) { - if (prefix) { - data = prefix + '\n' + data; - } - } else { - data = prefix; - } - - test('States test ' + testCount++ + ' prefix: ' + prefix, async function () { - await coreEditor.setValue(data, true); - const modes = statesAsList(); - expect(modes).toEqual(statesList); - }); - } - - statesTest( - ['start', 'json', 'json', 'start'], - 'POST _search\n' + '{\n' + ' "query": { "match_all": {} }\n' + '}' - ); - - statesTest( - ['start', 'json', ['json', 'json'], ['json', 'json'], 'json', 'start'], - 'POST _search\n' + '{\n' + ' "query": { \n' + ' "match_all": {} \n' + ' }\n' + '}' - ); - - statesTest( - ['start', 'json', 'json', 'start'], - 'POST _search\n' + '{\n' + ' "script": { "source": "" }\n' + '}' - ); - - statesTest( - ['start', 'json', 'json', 'start'], - 'POST _search\n' + '{\n' + ' "script": ""\n' + '}' - ); - - statesTest( - ['start', 'json', ['json', 'json'], 'json', 'start'], - 'POST _search\n' + '{\n' + ' "script": {\n' + ' }\n' + '}' - ); - - statesTest( - [ - 'start', - 'json', - ['script-start', 'json', 'json', 'json'], - ['script-start', 'json', 'json', 'json'], - ['json', 'json'], - 'json', - 'start', - ], - 'POST _search\n' + - '{\n' + - ' "test": { "script": """\n' + - ' test script\n' + - ' """\n' + - ' }\n' + - '}' - ); - - statesTest( - ['start', 'json', ['script-start', 'json'], ['script-start', 'json'], 'json', 'start'], - 'POST _search\n' + '{\n' + ' "script": """\n' + ' test script\n' + ' """,\n' + '}' - ); - - statesTest( - ['start', 'json', 'json', 'start'], - 'POST _search\n' + '{\n' + ' "script": """test script""",\n' + '}' - ); - - statesTest( - ['start', 'json', ['string_literal', 'json'], ['string_literal', 'json'], 'json', 'start'], - 'POST _search\n' + '{\n' + ' "something": """\n' + ' test script\n' + ' """,\n' + '}' - ); - - statesTest( - [ - 'start', - 'json', - ['string_literal', 'json', 'json', 'json'], - ['string_literal', 'json', 'json', 'json'], - ['json', 'json'], - ['json', 'json'], - 'json', - 'start', - ], - 'POST _search\n' + - '{\n' + - ' "something": { "f" : """\n' + - ' test script\n' + - ' """,\n' + - ' "g": 1\n' + - ' }\n' + - '}' - ); - - statesTest( - ['start', 'json', 'json', 'start'], - 'POST _search\n' + '{\n' + ' "something": """test script""",\n' + '}' - ); - }); -}); diff --git a/src/plugins/console/public/application/models/legacy_core_editor/legacy_core_editor.test.mocks.ts b/src/plugins/console/public/application/models/legacy_core_editor/legacy_core_editor.test.mocks.ts deleted file mode 100644 index 2ef5551e893d1..0000000000000 --- a/src/plugins/console/public/application/models/legacy_core_editor/legacy_core_editor.test.mocks.ts +++ /dev/null @@ -1,29 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the "Elastic License - * 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side - * Public License v 1"; you may not use this file except in compliance with, at - * your election, the "Elastic License 2.0", the "GNU Affero General Public - * License v3.0 only", or the "Server Side Public License, v 1". - */ - -jest.mock('./mode/worker', () => { - return { workerModule: { id: 'sense_editor/mode/worker', src: '' } }; -}); - -import '@kbn/web-worker-stub'; - -// @ts-ignore -window.URL = { - createObjectURL: () => { - return ''; - }, -}; - -import 'brace'; -import 'brace/ext/language_tools'; -import 'brace/ext/searchbox'; -import 'brace/mode/json'; -import 'brace/mode/text'; - -document.queryCommandSupported = () => true; diff --git a/src/plugins/console/public/application/models/legacy_core_editor/legacy_core_editor.ts b/src/plugins/console/public/application/models/legacy_core_editor/legacy_core_editor.ts deleted file mode 100644 index edeb64104be7f..0000000000000 --- a/src/plugins/console/public/application/models/legacy_core_editor/legacy_core_editor.ts +++ /dev/null @@ -1,511 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the "Elastic License - * 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side - * Public License v 1"; you may not use this file except in compliance with, at - * your election, the "Elastic License 2.0", the "GNU Affero General Public - * License v3.0 only", or the "Server Side Public License, v 1". - */ - -import ace, { type Annotation } from 'brace'; -import { Editor as IAceEditor, IEditSession as IAceEditSession } from 'brace'; -import $ from 'jquery'; -import { - CoreEditor, - Position, - Range, - Token, - TokensProvider, - EditorEvent, - AutoCompleterFunction, -} from '../../../types'; -import { AceTokensProvider } from '../../../lib/ace_token_provider'; -import * as curl from '../sense_editor/curl'; -import smartResize from './smart_resize'; -import * as InputMode from './mode/input'; - -const _AceRange = ace.acequire('ace/range').Range; - -const rangeToAceRange = ({ start, end }: Range) => - new _AceRange(start.lineNumber - 1, start.column - 1, end.lineNumber - 1, end.column - 1); - -export class LegacyCoreEditor implements CoreEditor { - private _aceOnPaste: Function; - $actions: JQuery; - resize: () => void; - - constructor(private readonly editor: IAceEditor, actions: HTMLElement) { - this.$actions = $(actions); - this.editor.setShowPrintMargin(false); - - const session = this.editor.getSession(); - // @ts-expect-error - // ignore ts error here due to type definition mistake in brace for setMode(mode: string): void; - // this method accepts string or SyntaxMode which is an object. See https://github.com/ajaxorg/ace/blob/13dc911dbc0ea31ca343d5744b3f472767458fc3/ace.d.ts#L467 - session.setMode(new InputMode.Mode()); - (session as unknown as { setFoldStyle: (style: string) => void }).setFoldStyle('markbeginend'); - session.setTabSize(2); - session.setUseWrapMode(true); - - this.resize = smartResize(this.editor); - - // Intercept ace on paste handler. - this._aceOnPaste = this.editor.onPaste; - this.editor.onPaste = this.DO_NOT_USE_onPaste.bind(this); - - this.editor.setOptions({ - enableBasicAutocompletion: true, - }); - - this.editor.$blockScrolling = Infinity; - this.hideActionsBar(); - this.editor.focus(); - } - - // dirty check for tokenizer state, uses a lot less cycles - // than listening for tokenizerUpdate - waitForLatestTokens(): Promise { - return new Promise((resolve) => { - const session = this.editor.getSession(); - const checkInterval = 25; - - const check = () => { - // If the bgTokenizer doesn't exist, we can assume that the underlying editor has been - // torn down, e.g. by closing the History tab, and we don't need to do anything further. - if (session.bgTokenizer) { - // Wait until the bgTokenizer is done running before executing the callback. - if ((session.bgTokenizer as unknown as { running: boolean }).running) { - setTimeout(check, checkInterval); - } else { - resolve(); - } - } - }; - - setTimeout(check, 0); - }); - } - - getLineState(lineNumber: number) { - const session = this.editor.getSession(); - return session.getState(lineNumber - 1); - } - - getValueInRange(range: Range): string { - return this.editor.getSession().getTextRange(rangeToAceRange(range)); - } - - getTokenProvider(): TokensProvider { - return new AceTokensProvider(this.editor.getSession()); - } - - getValue(): string { - return this.editor.getValue(); - } - - async setValue(text: string, forceRetokenize: boolean): Promise { - const session = this.editor.getSession(); - session.setValue(text); - if (forceRetokenize) { - await this.forceRetokenize(); - } - } - - getLineValue(lineNumber: number): string { - const session = this.editor.getSession(); - return session.getLine(lineNumber - 1); - } - - getCurrentPosition(): Position { - const cursorPosition = this.editor.getCursorPosition(); - return { - lineNumber: cursorPosition.row + 1, - column: cursorPosition.column + 1, - }; - } - - clearSelection(): void { - this.editor.clearSelection(); - } - - getTokenAt(pos: Position): Token | null { - const provider = this.getTokenProvider(); - return provider.getTokenAt(pos); - } - - insert(valueOrPos: string | Position, value?: string): void { - if (typeof valueOrPos === 'string') { - this.editor.insert(valueOrPos); - return; - } - const document = this.editor.getSession().getDocument(); - document.insert( - { - column: valueOrPos.column - 1, - row: valueOrPos.lineNumber - 1, - }, - value || '' - ); - } - - moveCursorToPosition(pos: Position): void { - this.editor.moveCursorToPosition({ row: pos.lineNumber - 1, column: pos.column - 1 }); - } - - replace(range: Range, value: string): void { - const session = this.editor.getSession(); - session.replace(rangeToAceRange(range), value); - } - - getLines(startLine: number, endLine: number): string[] { - const session = this.editor.getSession(); - return session.getLines(startLine - 1, endLine - 1); - } - - replaceRange(range: Range, value: string) { - const pos = this.editor.getCursorPosition(); - this.editor.getSession().replace(rangeToAceRange(range), value); - - const maxRow = Math.max(range.start.lineNumber - 1 + value.split('\n').length - 1, 1); - pos.row = Math.min(pos.row, maxRow); - this.editor.moveCursorToPosition(pos); - // ACE UPGRADE - check if needed - at the moment the above may trigger a selection. - this.editor.clearSelection(); - } - - getSelectionRange() { - const result = this.editor.getSelectionRange(); - return { - start: { - lineNumber: result.start.row + 1, - column: result.start.column + 1, - }, - end: { - lineNumber: result.end.row + 1, - column: result.end.column + 1, - }, - }; - } - - getLineCount() { - // Only use this function to return line count as it uses - // a cache. - return this.editor.getSession().getLength(); - } - - addMarker(range: Range) { - return this.editor - .getSession() - .addMarker(rangeToAceRange(range), 'ace_snippet-marker', 'fullLine', false); - } - - removeMarker(ref: number) { - this.editor.getSession().removeMarker(ref); - } - - getWrapLimit(): number { - return this.editor.getSession().getWrapLimit(); - } - - on(event: EditorEvent, listener: () => void) { - if (event === 'changeCursor') { - this.editor.getSession().selection.on(event, listener); - } else if (event === 'changeSelection') { - this.editor.on(event, listener); - } else { - this.editor.getSession().on(event, listener); - } - } - - off(event: EditorEvent, listener: () => void) { - if (event === 'changeSelection') { - this.editor.off(event, listener); - } - } - - isCompleterActive() { - return Boolean( - (this.editor as unknown as { completer: { activated: unknown } }).completer && - (this.editor as unknown as { completer: { activated: unknown } }).completer.activated - ); - } - - detachCompleter() { - // In some situations we need to detach the autocomplete suggestions element manually, - // such as when navigating away from Console when the suggestions list is open. - const completer = (this.editor as unknown as { completer: { detach(): void } }).completer; - return completer?.detach(); - } - - private forceRetokenize() { - const session = this.editor.getSession(); - return new Promise((resolve) => { - // force update of tokens, but not on this thread to allow for ace rendering. - setTimeout(function () { - let i; - for (i = 0; i < session.getLength(); i++) { - session.getTokens(i); - } - resolve(); - }); - }); - } - - // eslint-disable-next-line @typescript-eslint/naming-convention - private DO_NOT_USE_onPaste(text: string) { - if (text && curl.detectCURL(text)) { - const curlInput = curl.parseCURL(text); - this.editor.insert(curlInput); - return; - } - this._aceOnPaste.call(this.editor, text); - } - - private setActionsBar = (value: number | null, topOrBottom: 'top' | 'bottom' = 'top') => { - if (value === null) { - this.$actions.css('visibility', 'hidden'); - } else { - if (topOrBottom === 'top') { - this.$actions.css({ - bottom: 'auto', - top: value, - visibility: 'visible', - }); - } else { - this.$actions.css({ - top: 'auto', - bottom: value, - visibility: 'visible', - }); - } - } - }; - - private hideActionsBar = () => { - this.setActionsBar(null); - }; - - execCommand(cmd: string) { - this.editor.execCommand(cmd); - } - - getContainer(): HTMLDivElement { - return this.editor.container as HTMLDivElement; - } - - setStyles(styles: { wrapLines: boolean; fontSize: string }) { - this.editor.getSession().setUseWrapMode(styles.wrapLines); - this.editor.container.style.fontSize = styles.fontSize; - } - - registerKeyboardShortcut(opts: { keys: string; fn: () => void; name: string }): void { - this.editor.commands.addCommand({ - exec: opts.fn, - name: opts.name, - bindKey: opts.keys, - }); - } - - unregisterKeyboardShortcut(command: string) { - // @ts-ignore - this.editor.commands.removeCommand(command); - } - - legacyUpdateUI(range: Range) { - if (!this.$actions) { - return; - } - if (range) { - // elements are positioned relative to the editor's container - // pageY is relative to page, so subtract the offset - // from pageY to get the new top value - const offsetFromPage = $(this.editor.container).offset()!.top; - const startLine = range.start.lineNumber; - const startColumn = range.start.column; - const firstLine = this.getLineValue(startLine); - const maxLineLength = this.getWrapLimit() - 5; - const isWrapping = firstLine.length > maxLineLength; - const totalOffset = offsetFromPage - (window.pageYOffset || 0); - const getScreenCoords = (line: number) => - this.editor.renderer.textToScreenCoordinates(line - 1, startColumn).pageY - totalOffset; - const topOfReq = getScreenCoords(startLine); - - if (topOfReq >= 0) { - const { bottom: maxBottom } = this.editor.container.getBoundingClientRect(); - if (topOfReq > maxBottom - totalOffset) { - this.setActionsBar(0, 'bottom'); - return; - } - let offset = 0; - if (isWrapping) { - // Try get the line height of the text area in pixels. - const textArea = $(this.editor.container.querySelector('textArea')!); - const hasRoomOnNextLine = this.getLineValue(startLine).length < maxLineLength; - if (textArea && hasRoomOnNextLine) { - // Line height + the number of wraps we have on a line. - offset += this.getLineValue(startLine).length * textArea.height()!; - } else { - if (startLine > 1) { - this.setActionsBar(getScreenCoords(startLine - 1)); - return; - } - this.setActionsBar(getScreenCoords(startLine + 1)); - return; - } - } - this.setActionsBar(topOfReq + offset); - return; - } - - const bottomOfReq = - this.editor.renderer.textToScreenCoordinates(range.end.lineNumber, range.end.column).pageY - - offsetFromPage; - - if (bottomOfReq >= 0) { - this.setActionsBar(0); - return; - } - } - } - - registerAutocompleter(autocompleter: AutoCompleterFunction): void { - // Hook into Ace - - // disable standard context based autocompletion. - // @ts-ignore - ace.define( - 'ace/autocomplete/text_completer', - ['require', 'exports', 'module'], - function ( - require: unknown, - exports: { - getCompletions: ( - innerEditor: unknown, - session: unknown, - pos: unknown, - prefix: unknown, - callback: (e: null | Error, values: string[]) => void - ) => void; - } - ) { - exports.getCompletions = function (innerEditor, session, pos, prefix, callback) { - callback(null, []); - }; - } - ); - - const langTools = ace.acequire('ace/ext/language_tools'); - - langTools.setCompleters([ - { - identifierRegexps: [ - /[a-zA-Z_0-9\.\$\-\u00A2-\uFFFF]/, // adds support for dot character - ], - getCompletions: ( - // eslint-disable-next-line @typescript-eslint/naming-convention - DO_NOT_USE_1: IAceEditor, - aceEditSession: IAceEditSession, - pos: { row: number; column: number }, - prefix: string, - callback: (...args: unknown[]) => void - ) => { - const position: Position = { - lineNumber: pos.row + 1, - column: pos.column + 1, - }; - - const getAnnotationControls = () => { - let customAnnotation: Annotation; - return { - setAnnotation(text: string) { - const annotations = aceEditSession.getAnnotations(); - customAnnotation = { - text, - row: pos.row, - column: pos.column, - type: 'warning', - }; - - aceEditSession.setAnnotations([...annotations, customAnnotation]); - }, - removeAnnotation() { - aceEditSession.setAnnotations( - aceEditSession.getAnnotations().filter((a: Annotation) => a !== customAnnotation) - ); - }, - }; - }; - - autocompleter(position, prefix, callback, getAnnotationControls()); - }, - }, - ]); - } - - destroy() { - this.editor.destroy(); - } - - /** - * Formats body of the request in the editor by removing the extra whitespaces at the beginning of lines, - * And adds the correct indentation for each line - * @param reqRange request range to indent - */ - autoIndent(reqRange: Range) { - const session = this.editor.getSession(); - const mode = session.getMode(); - const startRow = reqRange.start.lineNumber; - const endRow = reqRange.end.lineNumber; - const tab = session.getTabString(); - - for (let row = startRow; row <= endRow; row++) { - let prevLineState = ''; - let prevLineIndent = ''; - if (row > 0) { - prevLineState = session.getState(row - 1); - const prevLine = session.getLine(row - 1); - prevLineIndent = mode.getNextLineIndent(prevLineState, prevLine, tab); - } - - const line = session.getLine(row); - // @ts-ignore - // Brace does not expose type definition for mode.$getIndent, though we have access to this method provided by the underlying Ace editor. - // See https://github.com/ajaxorg/ace/blob/87ce087ed1cf20eeabe56fb0894e048d9bc9c481/lib/ace/mode/text.js#L259 - const currLineIndent = mode.$getIndent(line); - if (prevLineIndent !== currLineIndent) { - if (currLineIndent.length > 0) { - // If current line has indentation, remove it. - // Next we will add the correct indentation by looking at the previous line - const range = new _AceRange(row, 0, row, currLineIndent.length); - session.remove(range); - } - if (prevLineIndent.length > 0) { - // If previous line has indentation, add indentation at the current line - session.insert({ row, column: 0 }, prevLineIndent); - } - } - - // Lastly outdent any closing braces - mode.autoOutdent(prevLineState, session, row); - } - } - - getAllFoldRanges(): Range[] { - const session = this.editor.getSession(); - // @ts-ignore - // Brace does not expose type definition for session.getAllFolds, though we have access to this method provided by the underlying Ace editor. - // See https://github.com/ajaxorg/ace/blob/13dc911dbc0ea31ca343d5744b3f472767458fc3/ace.d.ts#L82 - return session.getAllFolds().map((fold) => fold.range); - } - - addFoldsAtRanges(foldRanges: Range[]) { - const session = this.editor.getSession(); - foldRanges.forEach((range) => { - try { - session.addFold('...', _AceRange.fromPoints(range.start, range.end)); - } catch (e) { - // ignore the error if a fold fails - } - }); - } -} diff --git a/src/plugins/console/public/application/models/legacy_core_editor/mode/input.ts b/src/plugins/console/public/application/models/legacy_core_editor/mode/input.ts deleted file mode 100644 index 450feec6e9c3d..0000000000000 --- a/src/plugins/console/public/application/models/legacy_core_editor/mode/input.ts +++ /dev/null @@ -1,79 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the "Elastic License - * 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side - * Public License v 1"; you may not use this file except in compliance with, at - * your election, the "Elastic License 2.0", the "GNU Affero General Public - * License v3.0 only", or the "Server Side Public License, v 1". - */ - -import ace from 'brace'; -import { workerModule } from './worker'; -import { ScriptMode } from './script'; - -const TextMode = ace.acequire('ace/mode/text').Mode; - -const MatchingBraceOutdent = ace.acequire('ace/mode/matching_brace_outdent').MatchingBraceOutdent; -const CstyleBehaviour = ace.acequire('ace/mode/behaviour/cstyle').CstyleBehaviour; -const CStyleFoldMode = ace.acequire('ace/mode/folding/cstyle').FoldMode; -const WorkerClient = ace.acequire('ace/worker/worker_client').WorkerClient; -const AceTokenizer = ace.acequire('ace/tokenizer').Tokenizer; - -import { InputHighlightRules } from './input_highlight_rules'; - -export class Mode extends TextMode { - constructor() { - super(); - this.$tokenizer = new AceTokenizer(new InputHighlightRules().getRules()); - this.$outdent = new MatchingBraceOutdent(); - this.$behaviour = new CstyleBehaviour(); - this.foldingRules = new CStyleFoldMode(); - this.createModeDelegates({ - 'script-': ScriptMode, - }); - } -} - -(function (this: Mode) { - this.getCompletions = function () { - // autocomplete is done by the autocomplete module. - return []; - }; - - this.getNextLineIndent = function (state: string, line: string, tab: string) { - let indent = this.$getIndent(line); - - if (state !== 'string_literal') { - const match = line.match(/^.*[\{\(\[]\s*$/); - if (match) { - indent += tab; - } - } - - return indent; - }; - - this.checkOutdent = function (state: unknown, line: string, input: string) { - return this.$outdent.checkOutdent(line, input); - }; - - this.autoOutdent = function (state: unknown, doc: string, row: string) { - this.$outdent.autoOutdent(doc, row); - }; - this.createWorker = function (session: { - getDocument: () => string; - setAnnotations: (arg0: unknown) => void; - }) { - const worker = new WorkerClient(['ace', 'sense_editor'], workerModule, 'SenseWorker'); - worker.attachToDocument(session.getDocument()); - worker.on('error', function (e: { data: unknown }) { - session.setAnnotations([e.data]); - }); - - worker.on('ok', function (anno: { data: unknown }) { - session.setAnnotations(anno.data); - }); - - return worker; - }; -}).call(Mode.prototype); diff --git a/src/plugins/console/public/application/models/legacy_core_editor/mode/input_highlight_rules.ts b/src/plugins/console/public/application/models/legacy_core_editor/mode/input_highlight_rules.ts deleted file mode 100644 index 8a2f64b3c71f4..0000000000000 --- a/src/plugins/console/public/application/models/legacy_core_editor/mode/input_highlight_rules.ts +++ /dev/null @@ -1,180 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the "Elastic License - * 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side - * Public License v 1"; you may not use this file except in compliance with, at - * your election, the "Elastic License 2.0", the "GNU Affero General Public - * License v3.0 only", or the "Server Side Public License, v 1". - */ - -import ace from 'brace'; -import { addXJsonToRules } from '@kbn/ace'; - -type Token = - | string - | { token?: string; regex?: string; next?: string; push?: boolean; include?: string }; - -export function addEOL( - tokens: Token[], - reg: string | RegExp, - nextIfEOL: string, - normalNext?: string -) { - if (typeof reg === 'object') { - reg = reg.source; - } - return [ - { token: tokens.concat(['whitespace']), regex: reg + '(\\s*)$', next: nextIfEOL }, - { token: tokens, regex: reg, next: normalNext }, - ]; -} - -export const mergeTokens = (...args: any[]) => [].concat.apply([], args); - -const TextHighlightRules = ace.acequire('ace/mode/text_highlight_rules').TextHighlightRules; -// translating this to monaco -export class InputHighlightRules extends TextHighlightRules { - constructor() { - super(); - this.$rules = { - // TODO - 'start-sql': [ - { token: 'whitespace', regex: '\\s+' }, - { token: 'paren.lparen', regex: '{', next: 'json-sql', push: true }, - { regex: '', next: 'start' }, - ], - start: mergeTokens( - [ - // done - { token: 'warning', regex: '#!.*$' }, - // done - { include: 'comments' }, - // done - { token: 'paren.lparen', regex: '{', next: 'json', push: true }, - ], - // done - addEOL(['method'], /([a-zA-Z]+)/, 'start', 'method_sep'), - [ - // done - { - token: 'whitespace', - regex: '\\s+', - }, - // done - { - token: 'text', - regex: '.+?', - }, - ] - ), - method_sep: mergeTokens( - // done - addEOL( - ['whitespace', 'url.protocol_host', 'url.slash'], - /(\s+)(https?:\/\/[^?\/,]+)(\/)/, - 'start', - 'url' - ), - // done - addEOL(['whitespace', 'variable.template'], /(\s+)(\${\w+})/, 'start', 'url'), - // done - addEOL(['whitespace', 'url.protocol_host'], /(\s+)(https?:\/\/[^?\/,]+)/, 'start', 'url'), - // done - addEOL(['whitespace', 'url.slash'], /(\s+)(\/)/, 'start', 'url'), - // done - addEOL(['whitespace'], /(\s+)/, 'start', 'url') - ), - url: mergeTokens( - // done - addEOL(['variable.template'], /(\${\w+})/, 'start'), - // TODO - addEOL(['url.part'], /(_sql)/, 'start-sql', 'url-sql'), - // done - addEOL(['url.part'], /([^?\/,\s]+)/, 'start'), - // done - addEOL(['url.comma'], /(,)/, 'start'), - // done - addEOL(['url.slash'], /(\/)/, 'start'), - // done - addEOL(['url.questionmark'], /(\?)/, 'start', 'urlParams'), - // done - addEOL(['whitespace', 'comment.punctuation', 'comment.line'], /(\s+)(\/\/)(.*$)/, 'start') - ), - urlParams: mergeTokens( - // done - addEOL(['url.param', 'url.equal', 'variable.template'], /([^&=]+)(=)(\${\w+})/, 'start'), - // done - addEOL(['url.param', 'url.equal', 'url.value'], /([^&=]+)(=)([^&]*)/, 'start'), - // done - addEOL(['url.param'], /([^&=]+)/, 'start'), - // done - addEOL(['url.amp'], /(&)/, 'start'), - // done - addEOL(['whitespace', 'comment.punctuation', 'comment.line'], /(\s+)(\/\/)(.*$)/, 'start') - ), - // TODO - 'url-sql': mergeTokens( - addEOL(['url.part'], /([^?\/,\s]+)/, 'start-sql'), - addEOL(['url.comma'], /(,)/, 'start-sql'), - addEOL(['url.slash'], /(\/)/, 'start-sql'), - addEOL(['url.questionmark'], /(\?)/, 'start-sql', 'urlParams-sql') - ), - // TODO - 'urlParams-sql': mergeTokens( - addEOL(['url.param', 'url.equal', 'url.value'], /([^&=]+)(=)([^&]*)/, 'start-sql'), - addEOL(['url.param'], /([^&=]+)/, 'start-sql'), - addEOL(['url.amp'], /(&)/, 'start-sql') - ), - /** - * Each key in this.$rules considered to be a state in state machine. Regular expressions define the tokens for the current state, as well as the transitions into another state. - * See for more details https://cloud9-sdk.readme.io/docs/highlighting-rules#section-defining-states - * * - * Define a state for comments, these comment rules then can be included in other states. E.g. in 'start' and 'json' states by including { include: 'comments' } - * This will avoid duplicating the same rules in other states - */ - comments: [ - { - // Capture a line comment, indicated by # - // done - token: ['comment.punctuation', 'comment.line'], - regex: /(#)(.*$)/, - }, - { - // Begin capturing a block comment, indicated by /* - // done - token: 'comment.punctuation', - regex: /\/\*/, - push: [ - { - // Finish capturing a block comment, indicated by */ - // done - token: 'comment.punctuation', - regex: /\*\//, - next: 'pop', - }, - { - // done - defaultToken: 'comment.block', - }, - ], - }, - { - // Capture a line comment, indicated by // - // done - token: ['comment.punctuation', 'comment.line'], - regex: /(\/\/)(.*$)/, - }, - ], - }; - - addXJsonToRules(this, 'json'); - // Add comment rules to json rule set - this.$rules.json.unshift({ include: 'comments' }); - - this.$rules.json.unshift({ token: 'variable.template', regex: /("\${\w+}")/ }); - - if (this instanceof InputHighlightRules) { - this.normalizeRules(); - } - } -} diff --git a/src/plugins/console/public/application/models/legacy_core_editor/mode/output.ts b/src/plugins/console/public/application/models/legacy_core_editor/mode/output.ts deleted file mode 100644 index df7f3c37d55ec..0000000000000 --- a/src/plugins/console/public/application/models/legacy_core_editor/mode/output.ts +++ /dev/null @@ -1,37 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the "Elastic License - * 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side - * Public License v 1"; you may not use this file except in compliance with, at - * your election, the "Elastic License 2.0", the "GNU Affero General Public - * License v3.0 only", or the "Server Side Public License, v 1". - */ - -import ace from 'brace'; - -import { OutputJsonHighlightRules } from './output_highlight_rules'; - -const JSONMode = ace.acequire('ace/mode/json').Mode; -const MatchingBraceOutdent = ace.acequire('ace/mode/matching_brace_outdent').MatchingBraceOutdent; -const CstyleBehaviour = ace.acequire('ace/mode/behaviour/cstyle').CstyleBehaviour; -const CStyleFoldMode = ace.acequire('ace/mode/folding/cstyle').FoldMode; -ace.acequire('ace/worker/worker_client'); -const AceTokenizer = ace.acequire('ace/tokenizer').Tokenizer; - -export class Mode extends JSONMode { - constructor() { - super(); - this.$tokenizer = new AceTokenizer(new OutputJsonHighlightRules().getRules()); - this.$outdent = new MatchingBraceOutdent(); - this.$behaviour = new CstyleBehaviour(); - this.foldingRules = new CStyleFoldMode(); - } -} - -(function (this: Mode) { - this.createWorker = function () { - return null; - }; - - this.$id = 'sense/mode/input'; -}).call(Mode.prototype); diff --git a/src/plugins/console/public/application/models/legacy_core_editor/mode/output_highlight_rules.test.ts b/src/plugins/console/public/application/models/legacy_core_editor/mode/output_highlight_rules.test.ts deleted file mode 100644 index a18841aa4dc17..0000000000000 --- a/src/plugins/console/public/application/models/legacy_core_editor/mode/output_highlight_rules.test.ts +++ /dev/null @@ -1,56 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the "Elastic License - * 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side - * Public License v 1"; you may not use this file except in compliance with, at - * your election, the "Elastic License 2.0", the "GNU Affero General Public - * License v3.0 only", or the "Server Side Public License, v 1". - */ - -import { mapStatusCodeToBadge } from './output_highlight_rules'; - -describe('mapStatusCodeToBadge', () => { - const testCases = [ - { - description: 'treats 100 as as default', - value: '# PUT test-index 100 Continue', - badge: 'badge.badge--default', - }, - { - description: 'treats 200 as success', - value: '# PUT test-index 200 OK', - badge: 'badge.badge--success', - }, - { - description: 'treats 301 as primary', - value: '# PUT test-index 301 Moved Permanently', - badge: 'badge.badge--primary', - }, - { - description: 'treats 400 as warning', - value: '# PUT test-index 404 Not Found', - badge: 'badge.badge--warning', - }, - { - description: 'treats 502 as danger', - value: '# PUT test-index 502 Bad Gateway', - badge: 'badge.badge--danger', - }, - { - description: 'treats unexpected numbers as danger', - value: '# PUT test-index 666 Demonic Invasion', - badge: 'badge.badge--danger', - }, - { - description: 'treats no numbers as undefined', - value: '# PUT test-index', - badge: undefined, - }, - ]; - - testCases.forEach(({ description, value, badge }) => { - test(description, () => { - expect(mapStatusCodeToBadge(value)).toBe(badge); - }); - }); -}); diff --git a/src/plugins/console/public/application/models/legacy_core_editor/mode/output_highlight_rules.ts b/src/plugins/console/public/application/models/legacy_core_editor/mode/output_highlight_rules.ts deleted file mode 100644 index 765ba3e263f22..0000000000000 --- a/src/plugins/console/public/application/models/legacy_core_editor/mode/output_highlight_rules.ts +++ /dev/null @@ -1,64 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the "Elastic License - * 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side - * Public License v 1"; you may not use this file except in compliance with, at - * your election, the "Elastic License 2.0", the "GNU Affero General Public - * License v3.0 only", or the "Server Side Public License, v 1". - */ - -import ace from 'brace'; -import 'brace/mode/json'; -import { addXJsonToRules } from '@kbn/ace'; - -const JsonHighlightRules = ace.acequire('ace/mode/json_highlight_rules').JsonHighlightRules; - -export const mapStatusCodeToBadge = (value?: string) => { - const regExpMatchArray = value?.match(/\d+/); - if (regExpMatchArray) { - const status = parseInt(regExpMatchArray[0], 10); - if (status <= 199) { - return 'badge.badge--default'; - } - if (status <= 299) { - return 'badge.badge--success'; - } - if (status <= 399) { - return 'badge.badge--primary'; - } - if (status <= 499) { - return 'badge.badge--warning'; - } - return 'badge.badge--danger'; - } -}; - -export class OutputJsonHighlightRules extends JsonHighlightRules { - constructor() { - super(); - this.$rules = {}; - addXJsonToRules(this, 'start'); - this.$rules.start.unshift( - { - token: 'warning', - regex: '#!.*$', - }, - { - token: 'comment', - // match a comment starting with a hash at the start of the line - // ignore status codes and status texts at the end of the line (e.g. # GET _search/foo 200, # GET _search/foo 200 OK) - regex: /#(.*?)(?=[1-5][0-9][0-9]\s(?:[\sA-Za-z]+)|(?:[1-5][0-9][0-9])|$)/, - }, - { - token: mapStatusCodeToBadge, - // match status codes and status texts at the end of the line (e.g. # GET _search/foo 200, # GET _search/foo 200 OK) - // this rule allows us to highlight them with the corresponding badge color (e.g. 200 OK -> badge.badge--success) - regex: /([1-5][0-9][0-9]\s?[\sA-Za-z]+$)/, - } - ); - - if (this instanceof OutputJsonHighlightRules) { - this.normalizeRules(); - } - } -} diff --git a/src/plugins/console/public/application/models/legacy_core_editor/mode/script.ts b/src/plugins/console/public/application/models/legacy_core_editor/mode/script.ts deleted file mode 100644 index f50b6d3abe8ab..0000000000000 --- a/src/plugins/console/public/application/models/legacy_core_editor/mode/script.ts +++ /dev/null @@ -1,48 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the "Elastic License - * 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side - * Public License v 1"; you may not use this file except in compliance with, at - * your election, the "Elastic License 2.0", the "GNU Affero General Public - * License v3.0 only", or the "Server Side Public License, v 1". - */ - -import ace from 'brace'; -import { ScriptHighlightRules } from '@kbn/ace'; - -const TextMode = ace.acequire('ace/mode/text').Mode; -const MatchingBraceOutdent = ace.acequire('ace/mode/matching_brace_outdent').MatchingBraceOutdent; -const CstyleBehaviour = ace.acequire('ace/mode/behaviour/cstyle').CstyleBehaviour; -const CStyleFoldMode = ace.acequire('ace/mode/folding/cstyle').FoldMode; -ace.acequire('ace/tokenizer'); - -export class ScriptMode extends TextMode { - constructor() { - super(); - this.$outdent = new MatchingBraceOutdent(); - this.$behaviour = new CstyleBehaviour(); - this.foldingRules = new CStyleFoldMode(); - } -} - -(function (this: ScriptMode) { - this.HighlightRules = ScriptHighlightRules; - - this.getNextLineIndent = function (state: unknown, line: string, tab: string) { - let indent = this.$getIndent(line); - const match = line.match(/^.*[\{\[]\s*$/); - if (match) { - indent += tab; - } - - return indent; - }; - - this.checkOutdent = function (state: unknown, line: string, input: string) { - return this.$outdent.checkOutdent(line, input); - }; - - this.autoOutdent = function (state: unknown, doc: string, row: string) { - this.$outdent.autoOutdent(doc, row); - }; -}).call(ScriptMode.prototype); diff --git a/src/plugins/console/public/application/models/legacy_core_editor/mode/worker/index.d.ts b/src/plugins/console/public/application/models/legacy_core_editor/mode/worker/index.d.ts deleted file mode 100644 index 8067bec3556ae..0000000000000 --- a/src/plugins/console/public/application/models/legacy_core_editor/mode/worker/index.d.ts +++ /dev/null @@ -1,10 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the "Elastic License - * 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side - * Public License v 1"; you may not use this file except in compliance with, at - * your election, the "Elastic License 2.0", the "GNU Affero General Public - * License v3.0 only", or the "Server Side Public License, v 1". - */ - -export declare const workerModule: { id: string; src: string }; diff --git a/src/plugins/console/public/application/models/legacy_core_editor/mode/worker/index.js b/src/plugins/console/public/application/models/legacy_core_editor/mode/worker/index.js deleted file mode 100644 index 23f636b79e1a6..0000000000000 --- a/src/plugins/console/public/application/models/legacy_core_editor/mode/worker/index.js +++ /dev/null @@ -1,15 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the "Elastic License - * 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side - * Public License v 1"; you may not use this file except in compliance with, at - * your election, the "Elastic License 2.0", the "GNU Affero General Public - * License v3.0 only", or the "Server Side Public License, v 1". - */ - -import src from '!!raw-loader!./worker'; - -export const workerModule = { - id: 'sense_editor/mode/worker', - src, -}; diff --git a/src/plugins/console/public/application/models/legacy_core_editor/mode/worker/worker.js b/src/plugins/console/public/application/models/legacy_core_editor/mode/worker/worker.js deleted file mode 100644 index 65567f377cc52..0000000000000 --- a/src/plugins/console/public/application/models/legacy_core_editor/mode/worker/worker.js +++ /dev/null @@ -1,2392 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the "Elastic License - * 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side - * Public License v 1"; you may not use this file except in compliance with, at - * your election, the "Elastic License 2.0", the "GNU Affero General Public - * License v3.0 only", or the "Server Side Public License, v 1". - */ - -/* @notice - * - * This product includes code that is based on Ace editor, which was available - * under a "BSD" license. - * - * Distributed under the BSD license: - * - * Copyright (c) 2010, Ajax.org B.V. - * All rights reserved. - * - * Redistribution and use in source and binary forms, with or without - * modification, are permitted provided that the following conditions are met: - * * Redistributions of source code must retain the above copyright - * notice, this list of conditions and the following disclaimer. - * * Redistributions in binary form must reproduce the above copyright - * notice, this list of conditions and the following disclaimer in the - * documentation and/or other materials provided with the distribution. - * * Neither the name of Ajax.org B.V. nor the - * names of its contributors may be used to endorse or promote products - * derived from this software without specific prior written permission. - * - * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND - * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED - * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE - * DISCLAIMED. IN NO EVENT SHALL AJAX.ORG B.V. BE LIABLE FOR ANY - * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES - * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; - * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND - * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT - * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS - * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - */ - -/* eslint-disable prettier/prettier,prefer-const,eqeqeq,import/no-commonjs,no-undef,no-sequences, - block-scoped-var,no-use-before-define,no-var,one-var,guard-for-in,new-cap,no-nested-ternary,no-redeclare, - no-unused-vars,no-extend-native,no-empty,camelcase,no-proto,@kbn/imports/no_unresolvable_imports */ -/* - This file is loaded up as a blob by Brace to hand to Ace to load as Jsonp - (hence the redefining of everything). It is based on the javascript - mode from the brace distro. -*/ -function init(window) { - function resolveModuleId(id, paths) { - for (let testPath = id, tail = ''; testPath;) { - let alias = paths[testPath]; - if ('string' === typeof alias) return alias + tail; - if (alias) - {return (alias.location.replace(/\/*$/, '/') + (tail || alias.main || alias.name));} - if (alias === !1) return ''; - let i = testPath.lastIndexOf('/'); - if (-1 === i) break; - (tail = testPath.substr(i) + tail), (testPath = testPath.slice(0, i)); - } - return id; - } - if ( - !( - (void 0 !== window.window && window.document) || - (window.acequire && window.define) - ) - ) { - window.console || - ((window.console = function () { - let msgs = Array.prototype.slice.call(arguments, 0); - postMessage({ type: 'log', data: msgs }); - }), - (window.console.error = window.console.warn = window.console.log = window.console.trace = - window.console)), - (window.window = window), - (window.ace = window), - (window.onerror = function (message, file, line, col, err) { - postMessage({ - type: 'error', - data: { - message: message, - data: err.data, - file: file, - line: line, - col: col, - stack: err.stack, - }, - }); - }), - (window.normalizeModule = function (parentId, moduleName) { - if (-1 !== moduleName.indexOf('!')) { - let chunks = moduleName.split('!'); - return ( - window.normalizeModule(parentId, chunks[0]) + - '!' + - window.normalizeModule(parentId, chunks[1]) - ); - } - if ('.' == moduleName.charAt(0)) { - let base = parentId - .split('/') - .slice(0, -1) - .join('/'); - for ( - moduleName = (base ? base + '/' : '') + moduleName; - -1 !== moduleName.indexOf('.') && previous != moduleName; - - ) { - var previous = moduleName; - moduleName = moduleName - .replace(/^\.\//, '') - .replace(/\/\.\//, '/') - .replace(/[^\/]+\/\.\.\//, ''); - } - } - return moduleName; - }), - (window.acequire = function acequire(parentId, id) { - if ((id || ((id = parentId), (parentId = null)), !id.charAt)) - {throw Error( - 'worker.js acequire() accepts only (parentId, id) as arguments' - );} - id = window.normalizeModule(parentId, id); - let module = window.acequire.modules[id]; - if (module) - {return ( - module.initialized || - ((module.initialized = !0), - (module.exports = module.factory().exports)), - module.exports - );} - if (!window.acequire.tlns) return console.log('unable to load ' + id); - let path = resolveModuleId(id, window.acequire.tlns); - return ( - '.js' != path.slice(-3) && (path += '.js'), - (window.acequire.id = id), - (window.acequire.modules[id] = {}), - importScripts(path), - window.acequire(parentId, id) - ); - }), - (window.acequire.modules = {}), - (window.acequire.tlns = {}), - (window.define = function (id, deps, factory) { - if ( - (2 == arguments.length - ? ((factory = deps), - 'string' !== typeof id && ((deps = id), (id = window.acequire.id))) - : 1 == arguments.length && - ((factory = id), (deps = []), (id = window.acequire.id)), - 'function' !== typeof factory) - ) - {return ( - (window.acequire.modules[id] = { - exports: factory, - initialized: !0, - }), - void 0 - );} - deps.length || (deps = ['require', 'exports', 'module']); - let req = function (childId) { - return window.acequire(id, childId); - }; - window.acequire.modules[id] = { - exports: {}, - factory: function () { - let module = this, - returnExports = factory.apply( - this, - deps.map(function (dep) { - switch (dep) { - case 'require': - return req; - case 'exports': - return module.exports; - case 'module': - return module; - default: - return req(dep); - } - }) - ); - return returnExports && (module.exports = returnExports), module; - }, - }; - }), - (window.define.amd = {}), - (acequire.tlns = {}), - (window.initBaseUrls = function (topLevelNamespaces) { - for (let i in topLevelNamespaces) - {acequire.tlns[i] = topLevelNamespaces[i];} - }), - (window.initSender = function () { - let EventEmitter = window.acequire('ace/lib/event_emitter') - .EventEmitter, - oop = window.acequire('ace/lib/oop'), - Sender = function () {}; - return ( - function () { - oop.implement(this, EventEmitter), - (this.callback = function (data, callbackId) { - postMessage({ type: 'call', id: callbackId, data: data }); - }), - (this.emit = function (name, data) { - postMessage({ type: 'event', name: name, data: data }); - }); - }.call(Sender.prototype), - new Sender() - ); - }); - let main = (window.main = null), - sender = (window.sender = null); - window.onmessage = function (e) { - let msg = e.data; - if (msg.event && sender) sender._signal(msg.event, msg.data); - else if (msg.command) - {if (main[msg.command]) main[msg.command].apply(main, msg.args); - else { - if (!window[msg.command]) - throw Error('Unknown command:' + msg.command); - window[msg.command].apply(window, msg.args); - }} - else if (msg.init) { - window.initBaseUrls(msg.tlns), - acequire('ace/lib/es5-shim'), - (sender = window.sender = window.initSender()); - let clazz = acequire(msg.module)[msg.classname]; - main = window.main = new clazz(sender); - } - }; - } -} -init(this); -ace.define('ace/lib/oop', ['require', 'exports', 'module'], function ( - acequire, - exports -) { - (exports.inherits = function (ctor, superCtor) { - (ctor.super_ = superCtor), - (ctor.prototype = Object.create(superCtor.prototype, { - constructor: { - value: ctor, - enumerable: !1, - writable: !0, - configurable: !0, - }, - })); - }), - (exports.mixin = function (obj, mixin) { - for (let key in mixin) obj[key] = mixin[key]; - return obj; - }), - (exports.implement = function (proto, mixin) { - exports.mixin(proto, mixin); - }); -}), -ace.define('ace/range', ['require', 'exports', 'module'], function ( - acequire, - exports -) { - let comparePoints = function (p1, p2) { - return p1.row - p2.row || p1.column - p2.column; - }, - Range = function (startRow, startColumn, endRow, endColumn) { - (this.start = { row: startRow, column: startColumn }), - (this.end = { row: endRow, column: endColumn }); - }; - (function () { - (this.isEqual = function (range) { - return ( - this.start.row === range.start.row && - this.end.row === range.end.row && - this.start.column === range.start.column && - this.end.column === range.end.column - ); - }), - (this.toString = function () { - return ( - 'Range: [' + - this.start.row + - '/' + - this.start.column + - '] -> [' + - this.end.row + - '/' + - this.end.column + - ']' - ); - }), - (this.contains = function (row, column) { - return 0 == this.compare(row, column); - }), - (this.compareRange = function (range) { - let cmp, - end = range.end, - start = range.start; - return ( - (cmp = this.compare(end.row, end.column)), - 1 == cmp - ? ((cmp = this.compare(start.row, start.column)), - 1 == cmp ? 2 : 0 == cmp ? 1 : 0) - : -1 == cmp - ? -2 - : ((cmp = this.compare(start.row, start.column)), - -1 == cmp ? -1 : 1 == cmp ? 42 : 0) - ); - }), - (this.comparePoint = function (p) { - return this.compare(p.row, p.column); - }), - (this.containsRange = function (range) { - return ( - 0 == this.comparePoint(range.start) && - 0 == this.comparePoint(range.end) - ); - }), - (this.intersects = function (range) { - let cmp = this.compareRange(range); - return -1 == cmp || 0 == cmp || 1 == cmp; - }), - (this.isEnd = function (row, column) { - return this.end.row == row && this.end.column == column; - }), - (this.isStart = function (row, column) { - return this.start.row == row && this.start.column == column; - }), - (this.setStart = function (row, column) { - 'object' === typeof row - ? ((this.start.column = row.column), (this.start.row = row.row)) - : ((this.start.row = row), (this.start.column = column)); - }), - (this.setEnd = function (row, column) { - 'object' === typeof row - ? ((this.end.column = row.column), (this.end.row = row.row)) - : ((this.end.row = row), (this.end.column = column)); - }), - (this.inside = function (row, column) { - return 0 == this.compare(row, column) - ? this.isEnd(row, column) || this.isStart(row, column) - ? !1 - : !0 - : !1; - }), - (this.insideStart = function (row, column) { - return 0 == this.compare(row, column) - ? this.isEnd(row, column) - ? !1 - : !0 - : !1; - }), - (this.insideEnd = function (row, column) { - return 0 == this.compare(row, column) - ? this.isStart(row, column) - ? !1 - : !0 - : !1; - }), - (this.compare = function (row, column) { - return this.isMultiLine() || row !== this.start.row - ? this.start.row > row - ? -1 - : row > this.end.row - ? 1 - : this.start.row === row - ? column >= this.start.column - ? 0 - : -1 - : this.end.row === row - ? this.end.column >= column - ? 0 - : 1 - : 0 - : this.start.column > column - ? -1 - : column > this.end.column - ? 1 - : 0; - }), - (this.compareStart = function (row, column) { - return this.start.row == row && this.start.column == column - ? -1 - : this.compare(row, column); - }), - (this.compareEnd = function (row, column) { - return this.end.row == row && this.end.column == column - ? 1 - : this.compare(row, column); - }), - (this.compareInside = function (row, column) { - return this.end.row == row && this.end.column == column - ? 1 - : this.start.row == row && this.start.column == column - ? -1 - : this.compare(row, column); - }), - (this.clipRows = function (firstRow, lastRow) { - if (this.end.row > lastRow) var end = { row: lastRow + 1, column: 0 }; - else if (firstRow > this.end.row) - {var end = { row: firstRow, column: 0 };} - if (this.start.row > lastRow) - {var start = { row: lastRow + 1, column: 0 };} - else if (firstRow > this.start.row) - {var start = { row: firstRow, column: 0 };} - return Range.fromPoints(start || this.start, end || this.end); - }), - (this.extend = function (row, column) { - let cmp = this.compare(row, column); - if (0 == cmp) return this; - if (-1 == cmp) var start = { row: row, column: column }; - else var end = { row: row, column: column }; - return Range.fromPoints(start || this.start, end || this.end); - }), - (this.isEmpty = function () { - return ( - this.start.row === this.end.row && - this.start.column === this.end.column - ); - }), - (this.isMultiLine = function () { - return this.start.row !== this.end.row; - }), - (this.clone = function () { - return Range.fromPoints(this.start, this.end); - }), - (this.collapseRows = function () { - return 0 == this.end.column - ? new Range( - this.start.row, - 0, - Math.max(this.start.row, this.end.row - 1), - 0 - ) - : new Range(this.start.row, 0, this.end.row, 0); - }), - (this.toScreenRange = function (session) { - let screenPosStart = session.documentToScreenPosition(this.start), - screenPosEnd = session.documentToScreenPosition(this.end); - return new Range( - screenPosStart.row, - screenPosStart.column, - screenPosEnd.row, - screenPosEnd.column - ); - }), - (this.moveBy = function (row, column) { - (this.start.row += row), - (this.start.column += column), - (this.end.row += row), - (this.end.column += column); - }); - }.call(Range.prototype), - (Range.fromPoints = function (start, end) { - return new Range(start.row, start.column, end.row, end.column); - }), - (Range.comparePoints = comparePoints), - (Range.comparePoints = function (p1, p2) { - return p1.row - p2.row || p1.column - p2.column; - }), - (exports.Range = Range)); -}), -ace.define('ace/apply_delta', ['require', 'exports', 'module'], function ( - acequire, - exports -) { - exports.applyDelta = function (docLines, delta) { - let row = delta.start.row, - startColumn = delta.start.column, - line = docLines[row] || ''; - switch (delta.action) { - case 'insert': - var lines = delta.lines; - if (1 === lines.length) - {docLines[row] = - line.substring(0, startColumn) + - delta.lines[0] + - line.substring(startColumn);} - else { - let args = [row, 1].concat(delta.lines); - docLines.splice.apply(docLines, args), - (docLines[row] = line.substring(0, startColumn) + docLines[row]), - (docLines[row + delta.lines.length - 1] += line.substring( - startColumn - )); - } - break; - case 'remove': - var endColumn = delta.end.column, - endRow = delta.end.row; - row === endRow - ? (docLines[row] = - line.substring(0, startColumn) + line.substring(endColumn)) - : docLines.splice( - row, - endRow - row + 1, - line.substring(0, startColumn) + - docLines[endRow].substring(endColumn) - ); - } - }; -}), -ace.define( - 'ace/lib/event_emitter', - ['require', 'exports', 'module'], - function (acequire, exports) { - let EventEmitter = {}, - stopPropagation = function () { - this.propagationStopped = !0; - }, - preventDefault = function () { - this.defaultPrevented = !0; - }; - (EventEmitter._emit = EventEmitter._dispatchEvent = function ( - eventName, - e - ) { - this._eventRegistry || (this._eventRegistry = {}), - this._defaultHandlers || (this._defaultHandlers = {}); - let listeners = this._eventRegistry[eventName] || [], - defaultHandler = this._defaultHandlers[eventName]; - if (listeners.length || defaultHandler) { - ('object' === typeof e && e) || (e = {}), - e.type || (e.type = eventName), - e.stopPropagation || (e.stopPropagation = stopPropagation), - e.preventDefault || (e.preventDefault = preventDefault), - (listeners = listeners.slice()); - for ( - let i = 0; - listeners.length > i && - (listeners[i](e, this), !e.propagationStopped); - i++ - ); - return defaultHandler && !e.defaultPrevented - ? defaultHandler(e, this) - : void 0; - } - }), - (EventEmitter._signal = function (eventName, e) { - let listeners = (this._eventRegistry || {})[eventName]; - if (listeners) { - listeners = listeners.slice(); - for (let i = 0; listeners.length > i; i++) listeners[i](e, this); - } - }), - (EventEmitter.once = function (eventName, callback) { - let _self = this; - callback && - this.addEventListener(eventName, function newCallback() { - _self.removeEventListener(eventName, newCallback), - callback.apply(null, arguments); - }); - }), - (EventEmitter.setDefaultHandler = function (eventName, callback) { - let handlers = this._defaultHandlers; - if ( - (handlers || - (handlers = this._defaultHandlers = { _disabled_: {} }), - handlers[eventName]) - ) { - let old = handlers[eventName], - disabled = handlers._disabled_[eventName]; - disabled || (handlers._disabled_[eventName] = disabled = []), - disabled.push(old); - let i = disabled.indexOf(callback); - -1 != i && disabled.splice(i, 1); - } - handlers[eventName] = callback; - }), - (EventEmitter.removeDefaultHandler = function (eventName, callback) { - let handlers = this._defaultHandlers; - if (handlers) { - let disabled = handlers._disabled_[eventName]; - if (handlers[eventName] == callback) - {handlers[eventName], - disabled && this.setDefaultHandler(eventName, disabled.pop());} - else if (disabled) { - let i = disabled.indexOf(callback); - -1 != i && disabled.splice(i, 1); - } - } - }), - (EventEmitter.on = EventEmitter.addEventListener = function ( - eventName, - callback, - capturing - ) { - this._eventRegistry = this._eventRegistry || {}; - let listeners = this._eventRegistry[eventName]; - return ( - listeners || (listeners = this._eventRegistry[eventName] = []), - -1 == listeners.indexOf(callback) && - listeners[capturing ? 'unshift' : 'push'](callback), - callback - ); - }), - (EventEmitter.off = EventEmitter.removeListener = EventEmitter.removeEventListener = function ( - eventName, - callback - ) { - this._eventRegistry = this._eventRegistry || {}; - let listeners = this._eventRegistry[eventName]; - if (listeners) { - let index = listeners.indexOf(callback); - -1 !== index && listeners.splice(index, 1); - } - }), - (EventEmitter.removeAllListeners = function (eventName) { - this._eventRegistry && (this._eventRegistry[eventName] = []); - }), - (exports.EventEmitter = EventEmitter); - } -), -ace.define( - 'ace/anchor', - ['require', 'exports', 'module', 'ace/lib/oop', 'ace/lib/event_emitter'], - function (acequire, exports) { - let oop = acequire('./lib/oop'), - EventEmitter = acequire('./lib/event_emitter').EventEmitter, - Anchor = (exports.Anchor = function (doc, row, column) { - (this.$onChange = this.onChange.bind(this)), - this.attach(doc), - column === void 0 - ? this.setPosition(row.row, row.column) - : this.setPosition(row, column); - }); - (function () { - function $pointsInOrder(point1, point2, equalPointsInOrder) { - let bColIsAfter = equalPointsInOrder - ? point1.column <= point2.column - : point1.column < point2.column; - return ( - point1.row < point2.row || (point1.row == point2.row && bColIsAfter) - ); - } - function $getTransformedPoint(delta, point, moveIfEqual) { - let deltaIsInsert = 'insert' == delta.action, - deltaRowShift = - (deltaIsInsert ? 1 : -1) * (delta.end.row - delta.start.row), - deltaColShift = - (deltaIsInsert ? 1 : -1) * - (delta.end.column - delta.start.column), - deltaStart = delta.start, - deltaEnd = deltaIsInsert ? deltaStart : delta.end; - return $pointsInOrder(point, deltaStart, moveIfEqual) - ? { row: point.row, column: point.column } - : $pointsInOrder(deltaEnd, point, !moveIfEqual) - ? { - row: point.row + deltaRowShift, - column: - point.column + - (point.row == deltaEnd.row ? deltaColShift : 0), - } - : { row: deltaStart.row, column: deltaStart.column }; - } - oop.implement(this, EventEmitter), - (this.getPosition = function () { - return this.$clipPositionToDocument(this.row, this.column); - }), - (this.getDocument = function () { - return this.document; - }), - (this.$insertRight = !1), - (this.onChange = function (delta) { - if ( - !( - (delta.start.row == delta.end.row && - delta.start.row != this.row) || - delta.start.row > this.row - ) - ) { - let point = $getTransformedPoint( - delta, - { row: this.row, column: this.column }, - this.$insertRight - ); - this.setPosition(point.row, point.column, !0); - } - }), - (this.setPosition = function (row, column, noClip) { - let pos; - if ( - ((pos = noClip - ? { row: row, column: column } - : this.$clipPositionToDocument(row, column)), - this.row != pos.row || this.column != pos.column) - ) { - let old = { row: this.row, column: this.column }; - (this.row = pos.row), - (this.column = pos.column), - this._signal('change', { old: old, value: pos }); - } - }), - (this.detach = function () { - this.document.removeEventListener('change', this.$onChange); - }), - (this.attach = function (doc) { - (this.document = doc || this.document), - this.document.on('change', this.$onChange); - }), - (this.$clipPositionToDocument = function (row, column) { - let pos = {}; - return ( - row >= this.document.getLength() - ? ((pos.row = Math.max(0, this.document.getLength() - 1)), - (pos.column = this.document.getLine(pos.row).length)) - : 0 > row - ? ((pos.row = 0), (pos.column = 0)) - : ((pos.row = row), - (pos.column = Math.min( - this.document.getLine(pos.row).length, - Math.max(0, column) - ))), - 0 > column && (pos.column = 0), - pos - ); - }); - }.call(Anchor.prototype)); - } -), -ace.define( - 'ace/document', - [ - 'require', - 'exports', - 'module', - 'ace/lib/oop', - 'ace/apply_delta', - 'ace/lib/event_emitter', - 'ace/range', - 'ace/anchor', - ], - function (acequire, exports) { - let oop = acequire('./lib/oop'), - applyDelta = acequire('./apply_delta').applyDelta, - EventEmitter = acequire('./lib/event_emitter').EventEmitter, - Range = acequire('./range').Range, - Anchor = acequire('./anchor').Anchor, - Document = function (textOrLines) { - (this.$lines = ['']), - 0 === textOrLines.length - ? (this.$lines = ['']) - : Array.isArray(textOrLines) - ? this.insertMergedLines({ row: 0, column: 0 }, textOrLines) - : this.insert({ row: 0, column: 0 }, textOrLines); - }; - (function () { - oop.implement(this, EventEmitter), - (this.setValue = function (text) { - let len = this.getLength() - 1; - this.remove(new Range(0, 0, len, this.getLine(len).length)), - this.insert({ row: 0, column: 0 }, text); - }), - (this.getValue = function () { - return this.getAllLines().join(this.getNewLineCharacter()); - }), - (this.createAnchor = function (row, column) { - return new Anchor(this, row, column); - }), - (this.$split = - 0 === 'aaa'.split(/a/).length - ? function (text) { - return text.replace(/\r\n|\r/g, '\n').split('\n'); - } - : function (text) { - return text.split(/\r\n|\r|\n/); - }), - (this.$detectNewLine = function (text) { - let match = text.match(/^.*?(\r\n|\r|\n)/m); - (this.$autoNewLine = match ? match[1] : '\n'), - this._signal('changeNewLineMode'); - }), - (this.getNewLineCharacter = function () { - switch (this.$newLineMode) { - case 'windows': - return '\r\n'; - case 'unix': - return '\n'; - default: - return this.$autoNewLine || '\n'; - } - }), - (this.$autoNewLine = ''), - (this.$newLineMode = 'auto'), - (this.setNewLineMode = function (newLineMode) { - this.$newLineMode !== newLineMode && - ((this.$newLineMode = newLineMode), - this._signal('changeNewLineMode')); - }), - (this.getNewLineMode = function () { - return this.$newLineMode; - }), - (this.isNewLine = function (text) { - return '\r\n' == text || '\r' == text || '\n' == text; - }), - (this.getLine = function (row) { - return this.$lines[row] || ''; - }), - (this.getLines = function (firstRow, lastRow) { - return this.$lines.slice(firstRow, lastRow + 1); - }), - (this.getAllLines = function () { - return this.getLines(0, this.getLength()); - }), - (this.getLength = function () { - return this.$lines.length; - }), - (this.getTextRange = function (range) { - return this.getLinesForRange(range).join( - this.getNewLineCharacter() - ); - }), - (this.getLinesForRange = function (range) { - let lines; - if (range.start.row === range.end.row) - {lines = [ - this.getLine(range.start.row).substring( - range.start.column, - range.end.column - ), - ];} - else { - (lines = this.getLines(range.start.row, range.end.row)), - (lines[0] = (lines[0] || '').substring(range.start.column)); - let l = lines.length - 1; - range.end.row - range.start.row == l && - (lines[l] = lines[l].substring(0, range.end.column)); - } - return lines; - }), - (this.insertLines = function (row, lines) { - return ( - console.warn( - 'Use of document.insertLines is deprecated. Use the insertFullLines method instead.' - ), - this.insertFullLines(row, lines) - ); - }), - (this.removeLines = function (firstRow, lastRow) { - return ( - console.warn( - 'Use of document.removeLines is deprecated. Use the removeFullLines method instead.' - ), - this.removeFullLines(firstRow, lastRow) - ); - }), - (this.insertNewLine = function (position) { - return ( - console.warn( - 'Use of document.insertNewLine is deprecated. Use insertMergedLines(position, [\'\', \'\']) instead.' - ), - this.insertMergedLines(position, ['', '']) - ); - }), - (this.insert = function (position, text) { - return ( - 1 >= this.getLength() && this.$detectNewLine(text), - this.insertMergedLines(position, this.$split(text)) - ); - }), - (this.insertInLine = function (position, text) { - let start = this.clippedPos(position.row, position.column), - end = this.pos(position.row, position.column + text.length); - return ( - this.applyDelta( - { start: start, end: end, action: 'insert', lines: [text] }, - !0 - ), - this.clonePos(end) - ); - }), - (this.clippedPos = function (row, column) { - let length = this.getLength(); - void 0 === row - ? (row = length) - : 0 > row - ? (row = 0) - : row >= length && ((row = length - 1), (column = void 0)); - let line = this.getLine(row); - return ( - void 0 == column && (column = line.length), - (column = Math.min(Math.max(column, 0), line.length)), - { row: row, column: column } - ); - }), - (this.clonePos = function (pos) { - return { row: pos.row, column: pos.column }; - }), - (this.pos = function (row, column) { - return { row: row, column: column }; - }), - (this.$clipPosition = function (position) { - let length = this.getLength(); - return ( - position.row >= length - ? ((position.row = Math.max(0, length - 1)), - (position.column = this.getLine(length - 1).length)) - : ((position.row = Math.max(0, position.row)), - (position.column = Math.min( - Math.max(position.column, 0), - this.getLine(position.row).length - ))), - position - ); - }), - (this.insertFullLines = function (row, lines) { - row = Math.min(Math.max(row, 0), this.getLength()); - let column = 0; - this.getLength() > row - ? ((lines = lines.concat([''])), (column = 0)) - : ((lines = [''].concat(lines)), - row--, - (column = this.$lines[row].length)), - this.insertMergedLines({ row: row, column: column }, lines); - }), - (this.insertMergedLines = function (position, lines) { - let start = this.clippedPos(position.row, position.column), - end = { - row: start.row + lines.length - 1, - column: - (1 == lines.length ? start.column : 0) + - lines[lines.length - 1].length, - }; - return ( - this.applyDelta({ - start: start, - end: end, - action: 'insert', - lines: lines, - }), - this.clonePos(end) - ); - }), - (this.remove = function (range) { - let start = this.clippedPos(range.start.row, range.start.column), - end = this.clippedPos(range.end.row, range.end.column); - return ( - this.applyDelta({ - start: start, - end: end, - action: 'remove', - lines: this.getLinesForRange({ start: start, end: end }), - }), - this.clonePos(start) - ); - }), - (this.removeInLine = function (row, startColumn, endColumn) { - let start = this.clippedPos(row, startColumn), - end = this.clippedPos(row, endColumn); - return ( - this.applyDelta( - { - start: start, - end: end, - action: 'remove', - lines: this.getLinesForRange({ start: start, end: end }), - }, - !0 - ), - this.clonePos(start) - ); - }), - (this.removeFullLines = function (firstRow, lastRow) { - (firstRow = Math.min(Math.max(0, firstRow), this.getLength() - 1)), - (lastRow = Math.min(Math.max(0, lastRow), this.getLength() - 1)); - let deleteFirstNewLine = - lastRow == this.getLength() - 1 && firstRow > 0, - deleteLastNewLine = this.getLength() - 1 > lastRow, - startRow = deleteFirstNewLine ? firstRow - 1 : firstRow, - startCol = deleteFirstNewLine ? this.getLine(startRow).length : 0, - endRow = deleteLastNewLine ? lastRow + 1 : lastRow, - endCol = deleteLastNewLine ? 0 : this.getLine(endRow).length, - range = new Range(startRow, startCol, endRow, endCol), - deletedLines = this.$lines.slice(firstRow, lastRow + 1); - return ( - this.applyDelta({ - start: range.start, - end: range.end, - action: 'remove', - lines: this.getLinesForRange(range), - }), - deletedLines - ); - }), - (this.removeNewLine = function (row) { - this.getLength() - 1 > row && - row >= 0 && - this.applyDelta({ - start: this.pos(row, this.getLine(row).length), - end: this.pos(row + 1, 0), - action: 'remove', - lines: ['', ''], - }); - }), - (this.replace = function (range, text) { - if ( - (range instanceof Range || - (range = Range.fromPoints(range.start, range.end)), - 0 === text.length && range.isEmpty()) - ) - {return range.start;} - if (text == this.getTextRange(range)) return range.end; - this.remove(range); - let end; - return (end = text ? this.insert(range.start, text) : range.start); - }), - (this.applyDeltas = function (deltas) { - for (let i = 0; deltas.length > i; i++) this.applyDelta(deltas[i]); - }), - (this.revertDeltas = function (deltas) { - for (let i = deltas.length - 1; i >= 0; i--) - {this.revertDelta(deltas[i]);} - }), - (this.applyDelta = function (delta, doNotValidate) { - let isInsert = 'insert' == delta.action; - (isInsert - ? 1 >= delta.lines.length && !delta.lines[0] - : !Range.comparePoints(delta.start, delta.end)) || - (isInsert && - delta.lines.length > 2e4 && - this.$splitAndapplyLargeDelta(delta, 2e4), - applyDelta(this.$lines, delta, doNotValidate), - this._signal('change', delta)); - }), - (this.$splitAndapplyLargeDelta = function (delta, MAX) { - for ( - let lines = delta.lines, - l = lines.length, - row = delta.start.row, - column = delta.start.column, - from = 0, - to = 0; - ; - - ) { - (from = to), (to += MAX - 1); - let chunk = lines.slice(from, to); - if (to > l) { - (delta.lines = chunk), - (delta.start.row = row + from), - (delta.start.column = column); - break; - } - chunk.push(''), - this.applyDelta( - { - start: this.pos(row + from, column), - end: this.pos(row + to, (column = 0)), - action: delta.action, - lines: chunk, - }, - !0 - ); - } - }), - (this.revertDelta = function (delta) { - this.applyDelta({ - start: this.clonePos(delta.start), - end: this.clonePos(delta.end), - action: 'insert' == delta.action ? 'remove' : 'insert', - lines: delta.lines.slice(), - }); - }), - (this.indexToPosition = function (index, startRow) { - for ( - var lines = this.$lines || this.getAllLines(), - newlineLength = this.getNewLineCharacter().length, - i = startRow || 0, - l = lines.length; - l > i; - i++ - ) - {if (((index -= lines[i].length + newlineLength), 0 > index)) - return { - row: i, - column: index + lines[i].length + newlineLength, - };} - return { row: l - 1, column: lines[l - 1].length }; - }), - (this.positionToIndex = function (pos, startRow) { - for ( - var lines = this.$lines || this.getAllLines(), - newlineLength = this.getNewLineCharacter().length, - index = 0, - row = Math.min(pos.row, lines.length), - i = startRow || 0; - row > i; - ++i - ) - {index += lines[i].length + newlineLength;} - return index + pos.column; - }); - }.call(Document.prototype), - (exports.Document = Document)); - } -), -ace.define('ace/lib/lang', ['require', 'exports', 'module'], function ( - acequire, - exports -) { - (exports.last = function (a) { - return a[a.length - 1]; - }), - (exports.stringReverse = function (string) { - return string - .split('') - .reverse() - .join(''); - }), - (exports.stringRepeat = function (string, count) { - for (var result = ''; count > 0;) - {1 & count && (result += string), (count >>= 1) && (string += string);} - return result; - }); - let trimBeginRegexp = /^\s\s*/, - trimEndRegexp = /\s\s*$/; - (exports.stringTrimLeft = function (string) { - return string.replace(trimBeginRegexp, ''); - }), - (exports.stringTrimRight = function (string) { - return string.replace(trimEndRegexp, ''); - }), - (exports.copyObject = function (obj) { - let copy = {}; - for (let key in obj) copy[key] = obj[key]; - return copy; - }), - (exports.copyArray = function (array) { - for (var copy = [], i = 0, l = array.length; l > i; i++) - {copy[i] = - array[i] && 'object' == typeof array[i] - ? this.copyObject(array[i]) - : array[i];} - return copy; - }), - (exports.deepCopy = function deepCopy(obj) { - if ('object' !== typeof obj || !obj) return obj; - let copy; - if (Array.isArray(obj)) { - copy = []; - for (var key = 0; obj.length > key; key++) - {copy[key] = deepCopy(obj[key]);} - return copy; - } - if ('[object Object]' !== Object.prototype.toString.call(obj)) - {return obj;} - copy = {}; - for (var key in obj) copy[key] = deepCopy(obj[key]); - return copy; - }), - (exports.arrayToMap = function (arr) { - for (var map = {}, i = 0; arr.length > i; i++) map[arr[i]] = 1; - return map; - }), - (exports.createMap = function (props) { - let map = Object.create(null); - for (let i in props) map[i] = props[i]; - return map; - }), - (exports.arrayRemove = function (array, value) { - for (let i = 0; array.length >= i; i++) - {value === array[i] && array.splice(i, 1);} - }), - (exports.escapeRegExp = function (str) { - return str.replace(/([.*+?^${}()|[\]\/\\])/g, '\\$1'); - }), - (exports.escapeHTML = function (str) { - return str - .replace(/&/g, '&') - .replace(/"/g, '"') - .replace(/'/g, ''') - .replace(/ i; i += 2) { - if (Array.isArray(data[i + 1])) - var d = { - action: 'insert', - start: data[i], - lines: data[i + 1], - }; - else - var d = { - action: 'remove', - start: data[i], - end: data[i + 1], - }; - doc.applyDelta(d, !0); - }} - return _self.$timeout - ? deferredUpdate.schedule(_self.$timeout) - : (_self.onUpdate(), void 0); - }); - }); - (function () { - (this.$timeout = 500), - (this.setTimeout = function (timeout) { - this.$timeout = timeout; - }), - (this.setValue = function (value) { - this.doc.setValue(value), - this.deferredUpdate.schedule(this.$timeout); - }), - (this.getValue = function (callbackId) { - this.sender.callback(this.doc.getValue(), callbackId); - }), - (this.onUpdate = function () {}), - (this.isPending = function () { - return this.deferredUpdate.isPending(); - }); - }.call(Mirror.prototype)); - } -), -ace.define('ace/lib/es5-shim', ['require', 'exports', 'module'], function () { - function Empty() {} - function doesDefinePropertyWork(object) { - try { - return ( - Object.defineProperty(object, 'sentinel', {}), 'sentinel' in object - ); - } catch (exception) {} - } - function toInteger(n) { - return ( - (n = +n), - n !== n - ? (n = 0) - : 0 !== n && - n !== 1 / 0 && - n !== -(1 / 0) && - (n = (n > 0 || -1) * Math.floor(Math.abs(n))), - n - ); - } - Function.prototype.bind || - (Function.prototype.bind = function (that) { - let target = this; - if ('function' !== typeof target) - {throw new TypeError( - 'Function.prototype.bind called on incompatible ' + target - );} - var args = slice.call(arguments, 1), - bound = function () { - if (this instanceof bound) { - let result = target.apply( - this, - args.concat(slice.call(arguments)) - ); - return Object(result) === result ? result : this; - } - return target.apply(that, args.concat(slice.call(arguments))); - }; - return ( - target.prototype && - ((Empty.prototype = target.prototype), - (bound.prototype = new Empty()), - (Empty.prototype = null)), - bound - ); - }); - var defineGetter, - defineSetter, - lookupGetter, - lookupSetter, - supportsAccessors, - call = Function.prototype.call, - prototypeOfArray = Array.prototype, - prototypeOfObject = Object.prototype, - slice = prototypeOfArray.slice, - _toString = call.bind(prototypeOfObject.toString), - owns = call.bind(prototypeOfObject.hasOwnProperty); - if ( - ((supportsAccessors = owns(prototypeOfObject, '__defineGetter__')) && - ((defineGetter = call.bind(prototypeOfObject.__defineGetter__)), - (defineSetter = call.bind(prototypeOfObject.__defineSetter__)), - (lookupGetter = call.bind(prototypeOfObject.__lookupGetter__)), - (lookupSetter = call.bind(prototypeOfObject.__lookupSetter__))), - 2 != [1, 2].splice(0).length) - ) - {if ( - (function() { - function makeArray(l) { - var a = Array(l + 2); - return (a[0] = a[1] = 0), a; - } - var lengthBefore, - array = []; - return ( - array.splice.apply(array, makeArray(20)), - array.splice.apply(array, makeArray(26)), - (lengthBefore = array.length), - array.splice(5, 0, 'XXX'), - lengthBefore + 1 == array.length, - lengthBefore + 1 == array.length ? !0 : void 0 - ); - })() - ) { - var array_splice = Array.prototype.splice; - Array.prototype.splice = function(start, deleteCount) { - return arguments.length - ? array_splice.apply( - this, - [ - void 0 === start ? 0 : start, - void 0 === deleteCount ? this.length - start : deleteCount, - ].concat(slice.call(arguments, 2)) - ) - : []; - }; - } else - Array.prototype.splice = function(pos, removeCount) { - var length = this.length; - pos > 0 - ? pos > length && (pos = length) - : void 0 == pos - ? (pos = 0) - : 0 > pos && (pos = Math.max(length + pos, 0)), - length > pos + removeCount || (removeCount = length - pos); - var removed = this.slice(pos, pos + removeCount), - insert = slice.call(arguments, 2), - add = insert.length; - if (pos === length) add && this.push.apply(this, insert); - else { - var remove = Math.min(removeCount, length - pos), - tailOldPos = pos + remove, - tailNewPos = tailOldPos + add - remove, - tailCount = length - tailOldPos, - lengthAfterRemove = length - remove; - if (tailOldPos > tailNewPos) - for (var i = 0; tailCount > i; ++i) - this[tailNewPos + i] = this[tailOldPos + i]; - else if (tailNewPos > tailOldPos) - for (i = tailCount; i--; ) - this[tailNewPos + i] = this[tailOldPos + i]; - if (add && pos === lengthAfterRemove) - (this.length = lengthAfterRemove), this.push.apply(this, insert); - else - for (this.length = lengthAfterRemove + add, i = 0; add > i; ++i) - this[pos + i] = insert[i]; - } - return removed; - };} - Array.isArray || - (Array.isArray = function (obj) { - return '[object Array]' == _toString(obj); - }); - let boxedString = Object('a'), - splitString = 'a' != boxedString[0] || !(0 in boxedString); - if ( - (Array.prototype.forEach || - (Array.prototype.forEach = function (fun) { - let object = toObject(this), - self = - splitString && '[object String]' == _toString(this) - ? this.split('') - : object, - thisp = arguments[1], - i = -1, - length = self.length >>> 0; - if ('[object Function]' != _toString(fun)) throw new TypeError(); - for (; length > ++i;) - {i in self && fun.call(thisp, self[i], i, object);} - }), - Array.prototype.map || - (Array.prototype.map = function (fun) { - let object = toObject(this), - self = - splitString && '[object String]' == _toString(this) - ? this.split('') - : object, - length = self.length >>> 0, - result = Array(length), - thisp = arguments[1]; - if ('[object Function]' != _toString(fun)) - {throw new TypeError(fun + ' is not a function');} - for (let i = 0; length > i; i++) - {i in self && (result[i] = fun.call(thisp, self[i], i, object));} - return result; - }), - Array.prototype.filter || - (Array.prototype.filter = function (fun) { - let value, - object = toObject(this), - self = - splitString && '[object String]' == _toString(this) - ? this.split('') - : object, - length = self.length >>> 0, - result = [], - thisp = arguments[1]; - if ('[object Function]' != _toString(fun)) - {throw new TypeError(fun + ' is not a function');} - for (let i = 0; length > i; i++) - {i in self && - ((value = self[i]), - fun.call(thisp, value, i, object) && result.push(value));} - return result; - }), - Array.prototype.every || - (Array.prototype.every = function (fun) { - let object = toObject(this), - self = - splitString && '[object String]' == _toString(this) - ? this.split('') - : object, - length = self.length >>> 0, - thisp = arguments[1]; - if ('[object Function]' != _toString(fun)) - {throw new TypeError(fun + ' is not a function');} - for (let i = 0; length > i; i++) - {if (i in self && !fun.call(thisp, self[i], i, object)) return !1;} - return !0; - }), - Array.prototype.some || - (Array.prototype.some = function (fun) { - let object = toObject(this), - self = - splitString && '[object String]' == _toString(this) - ? this.split('') - : object, - length = self.length >>> 0, - thisp = arguments[1]; - if ('[object Function]' != _toString(fun)) - {throw new TypeError(fun + ' is not a function');} - for (let i = 0; length > i; i++) - {if (i in self && fun.call(thisp, self[i], i, object)) return !0;} - return !1; - }), - Array.prototype.reduce || - (Array.prototype.reduce = function (fun) { - let object = toObject(this), - self = - splitString && '[object String]' == _toString(this) - ? this.split('') - : object, - length = self.length >>> 0; - if ('[object Function]' != _toString(fun)) - {throw new TypeError(fun + ' is not a function');} - if (!length && 1 == arguments.length) - {throw new TypeError('reduce of empty array with no initial value');} - let result, - i = 0; - if (arguments.length >= 2) result = arguments[1]; - else - {for (;;) { - if (i in self) { - result = self[i++]; - break; - } - if (++i >= length) - throw new TypeError( - 'reduce of empty array with no initial value' - ); - }} - for (; length > i; i++) - {i in self && - (result = fun.call(void 0, result, self[i], i, object));} - return result; - }), - Array.prototype.reduceRight || - (Array.prototype.reduceRight = function (fun) { - let object = toObject(this), - self = - splitString && '[object String]' == _toString(this) - ? this.split('') - : object, - length = self.length >>> 0; - if ('[object Function]' != _toString(fun)) - {throw new TypeError(fun + ' is not a function');} - if (!length && 1 == arguments.length) - {throw new TypeError( - 'reduceRight of empty array with no initial value' - );} - let result, - i = length - 1; - if (arguments.length >= 2) result = arguments[1]; - else - {for (;;) { - if (i in self) { - result = self[i--]; - break; - } - if (0 > --i) - throw new TypeError( - 'reduceRight of empty array with no initial value' - ); - }} - do - {i in this && - (result = fun.call(void 0, result, self[i], i, object));} - while (i--); - return result; - }), - (Array.prototype.indexOf && -1 == [0, 1].indexOf(1, 2)) || - (Array.prototype.indexOf = function (sought) { - let self = - splitString && '[object String]' == _toString(this) - ? this.split('') - : toObject(this), - length = self.length >>> 0; - if (!length) return -1; - let i = 0; - for ( - arguments.length > 1 && (i = toInteger(arguments[1])), - i = i >= 0 ? i : Math.max(0, length + i); - length > i; - i++ - ) - {if (i in self && self[i] === sought) return i;} - return -1; - }), - (Array.prototype.lastIndexOf && -1 == [0, 1].lastIndexOf(0, -3)) || - (Array.prototype.lastIndexOf = function (sought) { - let self = - splitString && '[object String]' == _toString(this) - ? this.split('') - : toObject(this), - length = self.length >>> 0; - if (!length) return -1; - let i = length - 1; - for ( - arguments.length > 1 && (i = Math.min(i, toInteger(arguments[1]))), - i = i >= 0 ? i : length - Math.abs(i); - i >= 0; - i-- - ) - {if (i in self && sought === self[i]) return i;} - return -1; - }), - Object.getPrototypeOf || - (Object.getPrototypeOf = function (object) { - return ( - object.__proto__ || - (object.constructor - ? object.constructor.prototype - : prototypeOfObject) - ); - }), - !Object.getOwnPropertyDescriptor) - ) { - let ERR_NON_OBJECT = - 'Object.getOwnPropertyDescriptor called on a non-object: '; - Object.getOwnPropertyDescriptor = function (object, property) { - if ( - ('object' !== typeof object && 'function' !== typeof object) || - null === object - ) - {throw new TypeError(ERR_NON_OBJECT + object);} - if (owns(object, property)) { - var descriptor, getter, setter; - if ( - ((descriptor = { enumerable: !0, configurable: !0 }), - supportsAccessors) - ) { - let prototype = object.__proto__; - object.__proto__ = prototypeOfObject; - var getter = lookupGetter(object, property), - setter = lookupSetter(object, property); - if (((object.__proto__ = prototype), getter || setter)) - {return ( - getter && (descriptor.get = getter), - setter && (descriptor.set = setter), - descriptor - );} - } - return (descriptor.value = object[property]), descriptor; - } - }; - } - if ( - (Object.getOwnPropertyNames || - (Object.getOwnPropertyNames = function (object) { - return Object.keys(object); - }), - !Object.create) - ) { - let createEmpty; - (createEmpty = - null === Object.prototype.__proto__ - ? function () { - return { __proto__: null }; - } - : function () { - let empty = {}; - for (let i in empty) empty[i] = null; - return ( - (empty.constructor = empty.hasOwnProperty = empty.propertyIsEnumerable = empty.isPrototypeOf = empty.toLocaleString = empty.toString = empty.valueOf = empty.__proto__ = null), - empty - ); - }), - (Object.create = function (prototype, properties) { - let object; - if (null === prototype) object = createEmpty(); - else { - if ('object' !== typeof prototype) - {throw new TypeError( - 'typeof prototype[' + typeof prototype + "] != 'object'" - );} - let Type = function () {}; - (Type.prototype = prototype), - (object = new Type()), - (object.__proto__ = prototype); - } - return ( - void 0 !== properties && - Object.defineProperties(object, properties), - object - ); - }); - } - if (Object.defineProperty) { - let definePropertyWorksOnObject = doesDefinePropertyWork({}), - definePropertyWorksOnDom = - 'undefined' === typeof document || - doesDefinePropertyWork(document.createElement('div')); - if (!definePropertyWorksOnObject || !definePropertyWorksOnDom) - {var definePropertyFallback = Object.defineProperty;} - } - if (!Object.defineProperty || definePropertyFallback) { - let ERR_NON_OBJECT_DESCRIPTOR = - 'Property description must be an object: ', - ERR_NON_OBJECT_TARGET = 'Object.defineProperty called on non-object: ', - ERR_ACCESSORS_NOT_SUPPORTED = - 'getters & setters can not be defined on this javascript engine'; - Object.defineProperty = function (object, property, descriptor) { - if ( - ('object' !== typeof object && 'function' !== typeof object) || - null === object - ) - {throw new TypeError(ERR_NON_OBJECT_TARGET + object);} - if ( - ('object' !== typeof descriptor && 'function' !== typeof descriptor) || - null === descriptor - ) - {throw new TypeError(ERR_NON_OBJECT_DESCRIPTOR + descriptor);} - if (definePropertyFallback) - {try { - return definePropertyFallback.call( - Object, - object, - property, - descriptor - ); - } catch (exception) {}} - if (owns(descriptor, 'value')) - {if ( - supportsAccessors && - (lookupGetter(object, property) || lookupSetter(object, property)) - ) { - var prototype = object.__proto__; - (object.__proto__ = prototypeOfObject), - delete object[property], - (object[property] = descriptor.value), - (object.__proto__ = prototype); - } else object[property] = descriptor.value;} - else { - if (!supportsAccessors) - {throw new TypeError(ERR_ACCESSORS_NOT_SUPPORTED);} - owns(descriptor, 'get') && - defineGetter(object, property, descriptor.get), - owns(descriptor, 'set') && - defineSetter(object, property, descriptor.set); - } - return object; - }; - } - Object.defineProperties || - (Object.defineProperties = function (object, properties) { - for (let property in properties) - {owns(properties, property) && - Object.defineProperty(object, property, properties[property]);} - return object; - }), - Object.seal || - (Object.seal = function (object) { - return object; - }), - Object.freeze || - (Object.freeze = function (object) { - return object; - }); - try { - Object.freeze(function () {}); - } catch (exception) { - Object.freeze = (function (freezeObject) { - return function (object) { - return 'function' === typeof object ? object : freezeObject(object); - }; - }(Object.freeze)); - } - if ( - (Object.preventExtensions || - (Object.preventExtensions = function (object) { - return object; - }), - Object.isSealed || - (Object.isSealed = function () { - return !1; - }), - Object.isFrozen || - (Object.isFrozen = function () { - return !1; - }), - Object.isExtensible || - (Object.isExtensible = function (object) { - if (Object(object) === object) throw new TypeError(); - for (var name = ''; owns(object, name);) name += '?'; - object[name] = !0; - let returnValue = owns(object, name); - return delete object[name], returnValue; - }), - !Object.keys) - ) { - let hasDontEnumBug = !0, - dontEnums = [ - 'toString', - 'toLocaleString', - 'valueOf', - 'hasOwnProperty', - 'isPrototypeOf', - 'propertyIsEnumerable', - 'constructor', - ], - dontEnumsLength = dontEnums.length; - for (let key in { toString: null }) hasDontEnumBug = !1; - Object.keys = function (object) { - if ( - ('object' !== typeof object && 'function' !== typeof object) || - null === object - ) - {throw new TypeError('Object.keys called on a non-object');} - let keys = []; - for (let name in object) owns(object, name) && keys.push(name); - if (hasDontEnumBug) - {for (var i = 0, ii = dontEnumsLength; ii > i; i++) { - var dontEnum = dontEnums[i]; - owns(object, dontEnum) && keys.push(dontEnum); - }} - return keys; - }; - } - Date.now || - (Date.now = function () { - return new Date().getTime(); - }); - let ws = ' \nv\f\r   ᠎              \u2028\u2029'; - if (!String.prototype.trim || ws.trim()) { - ws = '[' + ws + ']'; - let trimBeginRegexp = RegExp('^' + ws + ws + '*'), - trimEndRegexp = RegExp(ws + ws + '*$'); - String.prototype.trim = function () { - return (this + '') - .replace(trimBeginRegexp, '') - .replace(trimEndRegexp, ''); - }; - } - var toObject = function (o) { - if (null == o) throw new TypeError('can\'t convert ' + o + ' to object'); - return Object(o); - }; -}); -ace.define( - 'sense_editor/mode/worker_parser', - ['require', 'exports', 'module'], - function () { - let at, // The index of the current character - ch, // The current character - annos, // annotations - escapee = { - '"': '"', - '\\': '\\', - '/': '/', - b: '\b', - f: '\f', - n: '\n', - r: '\r', - t: '\t', - }, - text, - annotate = function (type, text) { - annos.push({ type: type, text: text, at: at }); - }, - error = function (m) { - throw { - name: 'SyntaxError', - message: m, - at: at, - text: text, - }; - }, - reset = function (newAt) { - ch = text.charAt(newAt); - at = newAt + 1; - }, - next = function (c) { - if (c && c !== ch) { - error('Expected \'' + c + '\' instead of \'' + ch + '\''); - } - - ch = text.charAt(at); - at += 1; - return ch; - }, - nextUpTo = function (upTo, errorMessage) { - let currentAt = at, - i = text.indexOf(upTo, currentAt); - if (i < 0) { - error(errorMessage || 'Expected \'' + upTo + '\''); - } - reset(i + upTo.length); - return text.substring(currentAt, i); - }, - peek = function (offset) { - return text.charAt(at + offset); - }, - number = function () { - let number, - string = ''; - - if (ch === '-') { - string = '-'; - next('-'); - } - while (ch >= '0' && ch <= '9') { - string += ch; - next(); - } - if (ch === '.') { - string += '.'; - while (next() && ch >= '0' && ch <= '9') { - string += ch; - } - } - if (ch === 'e' || ch === 'E') { - string += ch; - next(); - if (ch === '-' || ch === '+') { - string += ch; - next(); - } - while (ch >= '0' && ch <= '9') { - string += ch; - next(); - } - } - number = +string; - if (isNaN(number)) { - error('Bad number'); - } else { - return number; - } - }, - string = function () { - let hex, - i, - string = '', - uffff; - - if (ch === '"') { - // If the current and the next characters are equal to "", empty string or start of triple quoted strings - if (peek(0) === '"' && peek(1) === '"') { - // literal - next('"'); - next('"'); - return nextUpTo('"""', 'failed to find closing \'"""\''); - } else { - while (next()) { - if (ch === '"') { - next(); - return string; - } else if (ch === '\\') { - next(); - if (ch === 'u') { - uffff = 0; - for (i = 0; i < 4; i += 1) { - hex = parseInt(next(), 16); - if (!isFinite(hex)) { - break; - } - uffff = uffff * 16 + hex; - } - string += String.fromCharCode(uffff); - } else if (typeof escapee[ch] === 'string') { - string += escapee[ch]; - } else { - break; - } - } else { - string += ch; - } - } - } - } - error('Bad string'); - }, - white = function () { - while (ch) { - // Skip whitespace. - while (ch && ch <= ' ') { - next(); - } - // if the current char in iteration is '#' or the char and the next char is equal to '//' - // we are on the single line comment - if (ch === '#' || ch === '/' && peek(0) === '/') { - // Until we are on the new line, skip to the next char - while (ch && ch !== '\n') { - next(); - } - } else if (ch === '/' && peek(0) === '*') { - // If the chars starts with '/*', we are on the multiline comment - next(); - next(); - while (ch && !(ch === '*' && peek(0) === '/')) { - // Until we have closing tags '*/', skip to the next char - next(); - } - if (ch) { - next(); - next(); - } - } else break; - } - }, - strictWhite = function () { - while (ch && (ch == ' ' || ch == '\t')) { - next(); - } - }, - newLine = function () { - if (ch == '\n') next(); - }, - word = function () { - switch (ch) { - case 't': - next('t'); - next('r'); - next('u'); - next('e'); - return true; - case 'f': - next('f'); - next('a'); - next('l'); - next('s'); - next('e'); - return false; - case 'n': - next('n'); - next('u'); - next('l'); - next('l'); - return null; - } - error('Unexpected \'' + ch + '\''); - }, - // parses and returns the method - method = function () { - switch (ch) { - case 'g': - next('g'); - next('e'); - next('t'); - return 'get'; - case 'G': - next('G'); - next('E'); - next('T'); - return 'GET'; - case 'h': - next('h'); - next('e'); - next('a'); - next('d'); - return 'head'; - case 'H': - next('H'); - next('E'); - next('A'); - next('D'); - return 'HEAD'; - case 'd': - next('d'); - next('e'); - next('l'); - next('e'); - next('t'); - next('e'); - return 'delete'; - case 'D': - next('D'); - next('E'); - next('L'); - next('E'); - next('T'); - next('E'); - return 'DELETE'; - case 'p': - next('p'); - switch (ch) { - case 'a': - next('a'); - next('t'); - next('c'); - next('h'); - return 'patch'; - case 'u': - next('u'); - next('t'); - return 'put'; - case 'o': - next('o'); - next('s'); - next('t'); - return 'post'; - default: - error('Unexpected \'' + ch + '\''); - } - break; - case 'P': - next('P'); - switch (ch) { - case 'A': - next('A'); - next('T'); - next('C'); - next('H'); - return 'PATCH'; - case 'U': - next('U'); - next('T'); - return 'PUT'; - case 'O': - next('O'); - next('S'); - next('T'); - return 'POST'; - default: - error('Unexpected \'' + ch + '\''); - } - break; - default: - error('Expected one of GET/POST/PUT/DELETE/HEAD/PATCH'); - } - }, - value, // Place holder for the value function. - array = function () { - const array = []; - - if (ch === '[') { - next('['); - white(); - if (ch === ']') { - next(']'); - return array; // empty array - } - while (ch) { - array.push(value()); - white(); - if (ch === ']') { - next(']'); - return array; - } - next(','); - white(); - } - } - error('Bad array'); - }, - object = function () { - let key, - object = {}; - - if (ch === '{') { - next('{'); - white(); - if (ch === '}') { - next('}'); - return object; // empty object - } - while (ch) { - key = string(); - white(); - next(':'); - if (Object.hasOwnProperty.call(object, key)) { - error('Duplicate key "' + key + '"'); - } - object[key] = value(); - white(); - if (ch === '}') { - next('}'); - return object; - } - next(','); - white(); - } - } - error('Bad object'); - }; - - value = function () { - white(); - switch (ch) { - case '{': - return object(); - case '[': - return array(); - case '"': - return string(); - case '-': - return number(); - default: - return ch >= '0' && ch <= '9' ? number() : word(); - } - }; - - let url = function () { - let url = ''; - while (ch && ch != '\n') { - url += ch; - next(); - } - if (url == '') { - error('Missing url'); - } - return url; - }, - request = function () { - white(); - method(); - strictWhite(); - url(); - strictWhite(); // advance to one new line - newLine(); - strictWhite(); - if (ch == '{') { - object(); - } - // multi doc request - strictWhite(); // advance to one new line - newLine(); - strictWhite(); - while (ch == '{') { - // another object - object(); - strictWhite(); - newLine(); - strictWhite(); - } - }, - comment = function () { - while (ch == '#') { - while (ch && ch !== '\n') { - next(); - } - white(); - } - }, - multi_request = function () { - while (ch && ch != '') { - white(); - if (!ch) { - continue; - } - try { - comment(); - white(); - if (!ch) { - continue; - } - request(); - white(); - } catch (e) { - annotate('error', e.message); - // snap - const substring = text.substr(at); - const nextMatch = substring.search(/^POST|HEAD|GET|PUT|DELETE|PATCH/m); - if (nextMatch < 1) return; - reset(at + nextMatch); - } - } - }; - - return function (source, reviver) { - let result; - - text = source; - at = 0; - annos = []; - next(); - multi_request(); - white(); - if (ch) { - annotate('error', 'Syntax error'); - } - - result = { annotations: annos }; - - return typeof reviver === 'function' - ? (function walk(holder, key) { - let k, - v, - value = holder[key]; - if (value && typeof value === 'object') { - for (k in value) { - if (Object.hasOwnProperty.call(value, k)) { - v = walk(value, k); - if (v !== undefined) { - value[k] = v; - } else { - delete value[k]; - } - } - } - } - return reviver.call(holder, key, value); - }({ '': result }, '')) - : result; - }; - } -); - -ace.define( - 'sense_editor/mode/worker', - [ - 'require', - 'exports', - 'module', - 'ace/lib/oop', - 'ace/worker/mirror', - 'sense_editor/mode/worker_parser', - ], - function (require, exports) { - const oop = require('ace/lib/oop'); - const Mirror = require('ace/worker/mirror').Mirror; - const parse = require('sense_editor/mode/worker_parser'); - - const SenseWorker = (exports.SenseWorker = function (sender) { - Mirror.call(this, sender); - this.setTimeout(200); - }); - - oop.inherits(SenseWorker, Mirror); - - (function () { - this.id = 'senseWorker'; - this.onUpdate = function () { - const value = this.doc.getValue(); - let pos, result; - try { - result = parse(value); - } catch (e) { - pos = this.charToDocumentPosition(e.at - 1); - this.sender.emit('error', { - row: pos.row, - column: pos.column, - text: e.message, - type: 'error', - }); - return; - } - for (let i = 0; i < result.annotations.length; i++) { - pos = this.charToDocumentPosition(result.annotations[i].at - 1); - result.annotations[i].row = pos.row; - result.annotations[i].column = pos.column; - } - this.sender.emit('ok', result.annotations); - }; - - this.charToDocumentPosition = function (charPos) { - let i = 0; - const len = this.doc.getLength(); - const nl = this.doc.getNewLineCharacter().length; - - if (!len) { - return { row: 0, column: 0 }; - } - - let lineStart = 0, - line; - while (i < len) { - line = this.doc.getLine(i); - const lineLength = line.length + nl; - if (lineStart + lineLength > charPos) { - return { - row: i, - column: charPos - lineStart, - }; - } - - lineStart += lineLength; - i += 1; - } - - return { - row: i - 1, - column: line.length, - }; - }; - }.call(SenseWorker.prototype)); - } -); diff --git a/src/plugins/console/public/application/models/legacy_core_editor/output_tokenization.test.js b/src/plugins/console/public/application/models/legacy_core_editor/output_tokenization.test.js deleted file mode 100644 index e09bf06e48246..0000000000000 --- a/src/plugins/console/public/application/models/legacy_core_editor/output_tokenization.test.js +++ /dev/null @@ -1,91 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the "Elastic License - * 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side - * Public License v 1"; you may not use this file except in compliance with, at - * your election, the "Elastic License 2.0", the "GNU Affero General Public - * License v3.0 only", or the "Server Side Public License, v 1". - */ - -import './legacy_core_editor.test.mocks'; -import $ from 'jquery'; -import RowParser from '../../../lib/row_parser'; -import ace from 'brace'; -import { createReadOnlyAceEditor } from './create_readonly'; -let output; -const tokenIterator = ace.acequire('ace/token_iterator'); - -describe('Output Tokenization', () => { - beforeEach(() => { - output = createReadOnlyAceEditor(document.querySelector('#ConAppOutput')); - $(output.container).show(); - }); - - afterEach(() => { - $(output.container).hide(); - }); - - function tokensAsList() { - const iter = new tokenIterator.TokenIterator(output.getSession(), 0, 0); - const ret = []; - let t = iter.getCurrentToken(); - const parser = new RowParser(output); - if (parser.isEmptyToken(t)) { - t = parser.nextNonEmptyToken(iter); - } - while (t) { - ret.push({ value: t.value, type: t.type }); - t = parser.nextNonEmptyToken(iter); - } - - return ret; - } - - let testCount = 0; - - function tokenTest(tokenList, data) { - if (data && typeof data !== 'string') { - data = JSON.stringify(data, null, 3); - } - - test('Token test ' + testCount++, function (done) { - output.update(data, function () { - const tokens = tokensAsList(); - const normTokenList = []; - for (let i = 0; i < tokenList.length; i++) { - normTokenList.push({ type: tokenList[i++], value: tokenList[i] }); - } - - expect(tokens).toEqual(normTokenList); - done(); - }); - }); - } - - tokenTest( - ['warning', '#! warning', 'comment', '# GET url', 'paren.lparen', '{', 'paren.rparen', '}'], - '#! warning\n' + '# GET url\n' + '{}' - ); - - tokenTest( - [ - 'comment', - '# GET url', - 'paren.lparen', - '{', - 'variable', - '"f"', - 'punctuation.colon', - ':', - 'punctuation.start_triple_quote', - '"""', - 'multi_string', - 'raw', - 'punctuation.end_triple_quote', - '"""', - 'paren.rparen', - '}', - ], - '# GET url\n' + '{ "f": """raw""" }' - ); -}); diff --git a/src/plugins/console/public/application/models/legacy_core_editor/smart_resize.ts b/src/plugins/console/public/application/models/legacy_core_editor/smart_resize.ts deleted file mode 100644 index c238e8c6a5da7..0000000000000 --- a/src/plugins/console/public/application/models/legacy_core_editor/smart_resize.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", the "GNU Affero General Public License v3.0 only", and the "Server Side - * Public License v 1"; you may not use this file except in compliance with, at - * your election, the "Elastic License 2.0", the "GNU Affero General Public - * License v3.0 only", or the "Server Side Public License, v 1". - */ - -import { get, throttle } from 'lodash'; -import type { Editor } from 'brace'; - -// eslint-disable-next-line import/no-default-export -export default function (editor: Editor) { - const resize = editor.resize; - - const throttledResize = throttle(() => { - resize.call(editor, false); - - // Keep current top line in view when resizing to avoid losing user context - const userRow = get(throttledResize, 'topRow', 0); - if (userRow !== 0) { - editor.renderer.scrollToLine(userRow, false, false, () => {}); - } - }, 35); - return throttledResize; -} diff --git a/src/plugins/console/public/application/models/legacy_core_editor/theme_sense_dark.js b/src/plugins/console/public/application/models/legacy_core_editor/theme_sense_dark.js deleted file mode 100644 index fd8e12bf1d703..0000000000000 --- a/src/plugins/console/public/application/models/legacy_core_editor/theme_sense_dark.js +++ /dev/null @@ -1,123 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the "Elastic License - * 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side - * Public License v 1"; you may not use this file except in compliance with, at - * your election, the "Elastic License 2.0", the "GNU Affero General Public - * License v3.0 only", or the "Server Side Public License, v 1". - */ - -import ace from 'brace'; - -ace.define('ace/theme/sense-dark', ['require', 'exports', 'module'], function (require, exports) { - exports.isDark = true; - exports.cssClass = 'ace-sense-dark'; - exports.cssText = - '.ace-sense-dark .ace_gutter {\ -background: #2e3236;\ -color: #bbbfc2;\ -}\ -.ace-sense-dark .ace_print-margin {\ -width: 1px;\ -background: #555651\ -}\ -.ace-sense-dark .ace_scroller {\ -background-color: #202328;\ -}\ -.ace-sense-dark .ace_content {\ -}\ -.ace-sense-dark .ace_text-layer {\ -color: #F8F8F2\ -}\ -.ace-sense-dark .ace_cursor {\ -border-left: 2px solid #F8F8F0\ -}\ -.ace-sense-dark .ace_overwrite-cursors .ace_cursor {\ -border-left: 0px;\ -border-bottom: 1px solid #F8F8F0\ -}\ -.ace-sense-dark .ace_marker-layer .ace_selection {\ -background: #222\ -}\ -.ace-sense-dark.ace_multiselect .ace_selection.ace_start {\ -box-shadow: 0 0 3px 0px #272822;\ -border-radius: 2px\ -}\ -.ace-sense-dark .ace_marker-layer .ace_step {\ -background: rgb(102, 82, 0)\ -}\ -.ace-sense-dark .ace_marker-layer .ace_bracket {\ -margin: -1px 0 0 -1px;\ -border: 1px solid #49483E\ -}\ -.ace-sense-dark .ace_marker-layer .ace_active-line {\ -background: #202020\ -}\ -.ace-sense-dark .ace_gutter-active-line {\ -background-color: #272727\ -}\ -.ace-sense-dark .ace_marker-layer .ace_selected-word {\ -border: 1px solid #49483E\ -}\ -.ace-sense-dark .ace_invisible {\ -color: #49483E\ -}\ -.ace-sense-dark .ace_entity.ace_name.ace_tag,\ -.ace-sense-dark .ace_keyword,\ -.ace-sense-dark .ace_meta,\ -.ace-sense-dark .ace_storage {\ -color: #F92672\ -}\ -.ace-sense-dark .ace_constant.ace_character,\ -.ace-sense-dark .ace_constant.ace_language,\ -.ace-sense-dark .ace_constant.ace_numeric,\ -.ace-sense-dark .ace_constant.ace_other {\ -color: #AE81FF\ -}\ -.ace-sense-dark .ace_invalid {\ -color: #F8F8F0;\ -background-color: #F92672\ -}\ -.ace-sense-dark .ace_invalid.ace_deprecated {\ -color: #F8F8F0;\ -background-color: #AE81FF\ -}\ -.ace-sense-dark .ace_support.ace_constant,\ -.ace-sense-dark .ace_support.ace_function {\ -color: #66D9EF\ -}\ -.ace-sense-dark .ace_fold {\ -background-color: #A6E22E;\ -border-color: #F8F8F2\ -}\ -.ace-sense-dark .ace_storage.ace_type,\ -.ace-sense-dark .ace_support.ace_class,\ -.ace-sense-dark .ace_support.ace_type {\ -font-style: italic;\ -color: #66D9EF\ -}\ -.ace-sense-dark .ace_entity.ace_name.ace_function,\ -.ace-sense-dark .ace_entity.ace_other.ace_attribute-name,\ -.ace-sense-dark .ace_variable {\ -color: #A6E22E\ -}\ -.ace-sense-dark .ace_variable.ace_parameter {\ -font-style: italic;\ -color: #FD971F\ -}\ -.ace-sense-dark .ace_string {\ -color: #E6DB74\ -}\ -.ace-sense-dark .ace_comment {\ -color: #629755\ -}\ -.ace-sense-dark .ace_markup.ace_underline {\ -text-decoration: underline\ -}\ -.ace-sense-dark .ace_indent-guide {\ -background: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAACCAYAAACZgbYnAAAAEklEQVQImWNQ11D6z7Bq1ar/ABCKBG6g04U2AAAAAElFTkSuQmCC) right repeat-y\ -}'; - - const dom = require('ace/lib/dom'); - dom.importCssString(exports.cssText, exports.cssClass); -}); 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 deleted file mode 100644 index 517f22bd8ad6a..0000000000000 --- a/src/plugins/console/public/application/models/sense_editor/__fixtures__/editor_input1.txt +++ /dev/null @@ -1,37 +0,0 @@ -GET _search -{ - "query": { "match_all": {} } -} - -#preceeding comment -GET _stats?level=shards - -#in between comment - -PUT index_1/type1/1 -{ - "f": 1 -} - -PUT index_1/type1/2 -{ - "f": 2 -} - -# comment - - -GET index_1/type1/1/_source?_source_include=f - -DELETE index_2 - - -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/create.ts b/src/plugins/console/public/application/models/sense_editor/create.ts deleted file mode 100644 index 9c6c3e38471d5..0000000000000 --- a/src/plugins/console/public/application/models/sense_editor/create.ts +++ /dev/null @@ -1,22 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the "Elastic License - * 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side - * Public License v 1"; you may not use this file except in compliance with, at - * your election, the "Elastic License 2.0", the "GNU Affero General Public - * License v3.0 only", or the "Server Side Public License, v 1". - */ - -import { SenseEditor } from './sense_editor'; -import * as core from '../legacy_core_editor'; - -export function create(element: HTMLElement) { - const coreEditor = core.create(element); - const senseEditor = new SenseEditor(coreEditor); - - /** - * Init the editor - */ - senseEditor.highlightCurrentRequestsAndUpdateActionBar(); - return senseEditor; -} diff --git a/src/plugins/console/public/application/models/sense_editor/curl.ts b/src/plugins/console/public/application/models/sense_editor/curl.ts deleted file mode 100644 index 9080610a0e8c5..0000000000000 --- a/src/plugins/console/public/application/models/sense_editor/curl.ts +++ /dev/null @@ -1,194 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the "Elastic License - * 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side - * Public License v 1"; you may not use this file except in compliance with, at - * your election, the "Elastic License 2.0", the "GNU Affero General Public - * License v3.0 only", or the "Server Side Public License, v 1". - */ - -function detectCURLinLine(line: string) { - // returns true if text matches a curl request - return line.match(/^\s*?curl\s+(-X[A-Z]+)?\s*['"]?.*?['"]?(\s*$|\s+?-d\s*?['"])/); -} - -export function detectCURL(text: string) { - // returns true if text matches a curl request - if (!text) return false; - for (const line of text.split('\n')) { - if (detectCURLinLine(line)) { - return true; - } - } - return false; -} - -export function parseCURL(text: string) { - let state = 'NONE'; - const out = []; - let body: string[] = []; - let line = ''; - const lines = text.trim().split('\n'); - let matches; - - const EmptyLine = /^\s*$/; - const Comment = /^\s*(?:#|\/{2,})(.*)\n?$/; - const ExecutionComment = /^\s*#!/; - const ClosingSingleQuote = /^([^']*)'/; - const ClosingDoubleQuote = /^((?:[^\\"]|\\.)*)"/; - const EscapedQuotes = /^((?:[^\\"']|\\.)+)/; - - const LooksLikeCurl = /^\s*curl\s+/; - const CurlVerb = /-X ?(GET|HEAD|POST|PUT|DELETE|PATCH)/; - - const HasProtocol = /[\s"']https?:\/\//; - const CurlRequestWithProto = /[\s"']https?:\/\/[^\/ ]+\/+([^\s"']+)/; - const CurlRequestWithoutProto = /[\s"'][^\/ ]+\/+([^\s"']+)/; - const CurlData = /^.+\s(--data|-d)\s*/; - const SenseLine = /^\s*(GET|HEAD|POST|PUT|DELETE|PATCH)\s+\/?(.+)/; - - if (lines.length > 0 && ExecutionComment.test(lines[0])) { - lines.shift(); - } - - function nextLine() { - if (line.length > 0) { - return true; - } - if (lines.length === 0) { - return false; - } - line = lines.shift()!.replace(/[\r\n]+/g, '\n') + '\n'; - return true; - } - - function unescapeLastBodyEl() { - const str = body.pop()!.replace(/\\([\\"'])/g, '$1'); - body.push(str); - } - - // Is the next char a single or double quote? - // If so remove it - function detectQuote() { - if (line.substr(0, 1) === "'") { - line = line.substr(1); - state = 'SINGLE_QUOTE'; - } else if (line.substr(0, 1) === '"') { - line = line.substr(1); - state = 'DOUBLE_QUOTE'; - } else { - state = 'UNQUOTED'; - } - } - - // Body is finished - append to output with final LF - function addBodyToOut() { - if (body.length > 0) { - out.push(body.join('')); - body = []; - } - state = 'LF'; - out.push('\n'); - } - - // If the pattern matches, then the state is about to change, - // so add the capture to the body and detect the next state - // Otherwise add the whole line - function consumeMatching(pattern: string | RegExp) { - const result = line.match(pattern); - if (result) { - body.push(result[1]); - line = line.substr(result[0].length); - detectQuote(); - } else { - body.push(line); - line = ''; - } - } - - function parseCurlLine() { - let verb = 'GET'; - let request = ''; - let result; - if ((result = line.match(CurlVerb))) { - verb = result[1]; - } - - // JS regexen don't support possessive quantifiers, so - // we need two distinct patterns - const pattern = HasProtocol.test(line) ? CurlRequestWithProto : CurlRequestWithoutProto; - - if ((result = line.match(pattern))) { - request = result[1]; - } - - out.push(verb + ' /' + request + '\n'); - - if ((result = line.match(CurlData))) { - line = line.substr(result[0].length); - detectQuote(); - if (EmptyLine.test(line)) { - line = ''; - } - } else { - state = 'NONE'; - line = ''; - out.push(''); - } - } - - while (nextLine()) { - if (state === 'SINGLE_QUOTE') { - consumeMatching(ClosingSingleQuote); - } else if (state === 'DOUBLE_QUOTE') { - consumeMatching(ClosingDoubleQuote); - unescapeLastBodyEl(); - } else if (state === 'UNQUOTED') { - consumeMatching(EscapedQuotes); - if (body.length) { - unescapeLastBodyEl(); - } - if (state === 'UNQUOTED') { - addBodyToOut(); - line = ''; - } - } - - // the BODY state (used to match the body of a Sense request) - // can be terminated early if it encounters - // a comment or an empty line - else if (state === 'BODY') { - if (Comment.test(line) || EmptyLine.test(line)) { - addBodyToOut(); - } else { - body.push(line); - line = ''; - } - } else if (EmptyLine.test(line)) { - if (state !== 'LF') { - out.push('\n'); - state = 'LF'; - } - line = ''; - } else if ((matches = line.match(Comment))) { - out.push('#' + matches[1] + '\n'); - state = 'NONE'; - line = ''; - } else if (LooksLikeCurl.test(line)) { - parseCurlLine(); - } else if ((matches = line.match(SenseLine))) { - out.push(matches[1] + ' /' + matches[2] + '\n'); - line = ''; - state = 'BODY'; - } - - // Nothing else matches, so output with a prefix of ### for debugging purposes - else { - out.push('### ' + line); - line = ''; - } - } - - addBodyToOut(); - return out.join('').trim(); -} diff --git a/src/plugins/console/public/application/models/sense_editor/index.ts b/src/plugins/console/public/application/models/sense_editor/index.ts deleted file mode 100644 index 2bd44988dc02f..0000000000000 --- a/src/plugins/console/public/application/models/sense_editor/index.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", the "GNU Affero General Public License v3.0 only", and the "Server Side - * Public License v 1"; you may not use this file except in compliance with, at - * your election, the "Elastic License 2.0", the "GNU Affero General Public - * License v3.0 only", or the "Server Side Public License, v 1". - */ - -export * from './create'; -export * from '../legacy_core_editor/create_readonly'; -export { MODE } from '../../../lib/row_parser'; -export { SenseEditor } from './sense_editor'; -export { getEndpointFromPosition } from '../../../lib/autocomplete/get_endpoint_from_position'; diff --git a/src/plugins/console/public/application/models/sense_editor/integration.test.js b/src/plugins/console/public/application/models/sense_editor/integration.test.js deleted file mode 100644 index bed83293e31d6..0000000000000 --- a/src/plugins/console/public/application/models/sense_editor/integration.test.js +++ /dev/null @@ -1,1279 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the "Elastic License - * 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side - * Public License v 1"; you may not use this file except in compliance with, at - * your election, the "Elastic License 2.0", the "GNU Affero General Public - * License v3.0 only", or the "Server Side Public License, v 1". - */ - -import './sense_editor.test.mocks'; -import { create } from './create'; -import _ from 'lodash'; -import $ from 'jquery'; - -import * as kb from '../../../lib/kb/kb'; -import { AutocompleteInfo, setAutocompleteInfo } from '../../../services'; -import { httpServiceMock } from '@kbn/core-http-browser-mocks'; -import { StorageMock } from '../../../services/storage.mock'; -import { SettingsMock } from '../../../services/settings.mock'; - -describe('Integration', () => { - let senseEditor; - let autocompleteInfo; - - beforeEach(() => { - // Set up our document body - document.body.innerHTML = - '
'; - - senseEditor = create(document.querySelector('#ConAppEditor')); - $(senseEditor.getCoreEditor().getContainer()).show(); - senseEditor.autocomplete._test.removeChangeListener(); - autocompleteInfo = new AutocompleteInfo(); - - const httpMock = httpServiceMock.createSetupContract(); - const storage = new StorageMock({}, 'test'); - const settingsMock = new SettingsMock(storage); - - settingsMock.getAutocomplete.mockReturnValue({ fields: true }); - - autocompleteInfo.mapping.setup(httpMock, settingsMock); - - setAutocompleteInfo(autocompleteInfo); - }); - afterEach(() => { - $(senseEditor.getCoreEditor().getContainer()).hide(); - senseEditor.autocomplete._test.addChangeListener(); - autocompleteInfo = null; - setAutocompleteInfo(null); - }); - - function processContextTest(data, mapping, kbSchemes, requestLine, testToRun) { - test(testToRun.name, function (done) { - let lineOffset = 0; // add one for the extra method line - let editorValue = data; - if (requestLine != null) { - if (data != null) { - editorValue = requestLine + '\n' + data; - lineOffset = 1; - } else { - editorValue = requestLine; - } - } - - testToRun.cursor.lineNumber += lineOffset; - - autocompleteInfo.clear(); - autocompleteInfo.mapping.loadMappings(mapping); - const json = {}; - json[test.name] = kbSchemes || {}; - const testApi = kb._test.loadApisFromJson(json); - if (kbSchemes) { - // if (kbSchemes.globals) { - // $.each(kbSchemes.globals, function (parent, rules) { - // testApi.addGlobalAutocompleteRules(parent, rules); - // }); - // } - if (kbSchemes.endpoints) { - $.each(kbSchemes.endpoints, function (endpoint, scheme) { - testApi.addEndpointDescription(endpoint, scheme); - }); - } - } - kb._test.setActiveApi(testApi); - const { cursor } = testToRun; - senseEditor.update(editorValue, true).then(() => { - senseEditor.getCoreEditor().moveCursorToPosition(cursor); - // allow ace rendering to move cursor so it will be seen during test - handy for debugging. - //setTimeout(function () { - senseEditor.completer = { - base: {}, - changeListener: function () {}, - }; // mimic auto complete - - senseEditor.autocomplete._test.getCompletions( - senseEditor, - null, - cursor, - '', - function (err, terms) { - if (testToRun.assertThrows) { - done(); - return; - } - - if (err) { - throw err; - } - - if (testToRun.no_context) { - expect(!terms || terms.length === 0).toBeTruthy(); - } else { - expect(terms).not.toBeNull(); - expect(terms.length).toBeGreaterThan(0); - } - - if (!terms || terms.length === 0) { - done(); - return; - } - - if (testToRun.autoCompleteSet) { - const expectedTerms = _.map(testToRun.autoCompleteSet, function (t) { - if (typeof t !== 'object') { - t = { name: t }; - } - return t; - }); - if (terms.length !== expectedTerms.length) { - expect(_.map(terms, 'name')).toEqual(_.map(expectedTerms, 'name')); - } else { - const filteredActualTerms = _.map(terms, function (actualTerm, i) { - const expectedTerm = expectedTerms[i]; - const filteredTerm = {}; - _.each(expectedTerm, function (v, p) { - filteredTerm[p] = actualTerm[p]; - }); - return filteredTerm; - }); - expect(filteredActualTerms).toEqual(expectedTerms); - } - } - - const context = terms[0].context; - const { - cursor: { lineNumber, column }, - } = testToRun; - senseEditor.autocomplete._test.addReplacementInfoToContext( - context, - { lineNumber, column }, - terms[0].value - ); - - function ac(prop, propTest) { - if (typeof testToRun[prop] !== 'undefined') { - if (propTest) { - propTest(context[prop], testToRun[prop], prop); - } else { - expect(context[prop]).toEqual(testToRun[prop]); - } - } - } - - function posCompare(actual, expected) { - expect(actual.lineNumber).toEqual(expected.lineNumber + lineOffset); - expect(actual.column).toEqual(expected.column); - } - - function rangeCompare(actual, expected, name) { - posCompare(actual.start, expected.start, name + '.start'); - posCompare(actual.end, expected.end, name + '.end'); - } - - ac('prefixToAdd'); - ac('suffixToAdd'); - ac('addTemplate'); - ac('textBoxPosition', posCompare); - ac('rangeToReplace', rangeCompare); - done(); - }, - { setAnnotation: () => {}, removeAnnotation: () => {} } - ); - }); - }); - } - - function contextTests(data, mapping, kbSchemes, requestLine, tests) { - if (data != null && typeof data !== 'string') { - data = JSON.stringify(data, null, 3); - } - for (let t = 0; t < tests.length; t++) { - processContextTest(data, mapping, kbSchemes, requestLine, tests[t]); - } - } - - const SEARCH_KB = { - endpoints: { - _search: { - methods: ['GET', 'POST'], - patterns: ['{index}/_search', '_search'], - data_autocomplete_rules: { - query: { - match_all: {}, - term: { '{field}': { __template: { f: 1 } } }, - }, - size: {}, - facets: { - __template: { - FIELD: {}, - }, - '*': { terms: { field: '{field}' } }, - }, - }, - }, - }, - }; - - const MAPPING = { - index1: { - properties: { - 'field1.1.1': { type: 'string' }, - 'field1.1.2': { type: 'string' }, - }, - }, - index2: { - properties: { - 'field2.1.1': { type: 'string' }, - 'field2.1.2': { type: 'string' }, - }, - }, - }; - - contextTests({}, MAPPING, SEARCH_KB, 'POST _search', [ - { - name: 'Empty doc', - cursor: { lineNumber: 1, column: 2 }, - initialValue: '', - addTemplate: true, - prefixToAdd: '', - suffixToAdd: '', - rangeToReplace: { - start: { lineNumber: 1, column: 2 }, - end: { lineNumber: 1, column: 2 }, - }, - autoCompleteSet: ['facets', 'query', 'size'], - }, - ]); - - contextTests({}, MAPPING, SEARCH_KB, 'POST _no_context', [ - { - name: 'Missing KB', - cursor: { lineNumber: 1, column: 2 }, - no_context: true, - }, - ]); - - contextTests( - { - query: { - f: 1, - }, - }, - MAPPING, - { - globals: { - query: { - t1: 2, - }, - }, - endpoints: {}, - }, - 'POST _no_context', - [ - { - name: 'Missing KB - global auto complete', - cursor: { lineNumber: 3, column: 6 }, - autoCompleteSet: ['t1'], - }, - ] - ); - - contextTests( - { - query: { - field: 'something', - }, - facets: {}, - size: 20, - }, - MAPPING, - SEARCH_KB, - 'POST _search', - [ - { - name: 'existing dictionary key, no template', - cursor: { lineNumber: 2, column: 6 }, - initialValue: 'query', - addTemplate: false, - prefixToAdd: '', - suffixToAdd: '', - rangeToReplace: { - start: { lineNumber: 2, column: 4 }, - end: { lineNumber: 2, column: 11 }, - }, - autoCompleteSet: ['facets', 'query', 'size'], - }, - { - name: 'existing inner dictionary key', - cursor: { lineNumber: 3, column: 8 }, - initialValue: 'field', - addTemplate: false, - prefixToAdd: '', - suffixToAdd: '', - rangeToReplace: { - start: { lineNumber: 3, column: 7 }, - end: { lineNumber: 3, column: 14 }, - }, - autoCompleteSet: ['match_all', 'term'], - }, - { - name: 'existing dictionary key, yes template', - cursor: { lineNumber: 5, column: 8 }, - initialValue: 'facets', - addTemplate: true, - prefixToAdd: '', - suffixToAdd: '', - rangeToReplace: { - start: { lineNumber: 5, column: 4 }, - end: { lineNumber: 5, column: 16 }, - }, - autoCompleteSet: ['facets', 'query', 'size'], - }, - { - name: 'ignoring meta keys', - cursor: { lineNumber: 5, column: 15 }, - no_context: true, - }, - ] - ); - - contextTests( - '{\n' + - ' "query": {\n' + - ' "field": "something"\n' + - ' },\n' + - ' "facets": {},\n' + - ' "size": 20\n' + - '}', - MAPPING, - SEARCH_KB, - 'POST _search', - [ - { - name: 'trailing comma, end of line', - cursor: { lineNumber: 5, column: 17 }, - initialValue: '', - addTemplate: true, - prefixToAdd: '', - suffixToAdd: ', ', - rangeToReplace: { - start: { lineNumber: 5, column: 17 }, - end: { lineNumber: 5, column: 17 }, - }, - autoCompleteSet: ['facets', 'query', 'size'], - }, - { - name: 'trailing comma, beginning of line', - cursor: { lineNumber: 6, column: 2 }, - initialValue: '', - addTemplate: true, - prefixToAdd: '', - suffixToAdd: ', ', - rangeToReplace: { - start: { lineNumber: 6, column: 2 }, - end: { lineNumber: 6, column: 2 }, - }, - autoCompleteSet: ['facets', 'query', 'size'], - }, - { - name: 'prefix comma, end of line', - cursor: { lineNumber: 7, column: 1 }, - initialValue: '', - addTemplate: true, - prefixToAdd: ',\n', - suffixToAdd: '', - rangeToReplace: { - start: { lineNumber: 6, column: 14 }, - end: { lineNumber: 7, column: 1 }, - }, - autoCompleteSet: ['facets', 'query', 'size'], - }, - ] - ); - - contextTests( - { - object: 1, - array: 1, - value_one_of: 1, - value: 2, - something_else: 5, - }, - MAPPING, - { - endpoints: { - _test: { - patterns: ['_test'], - data_autocomplete_rules: { - object: { bla: 1 }, - array: [1], - value_one_of: { __one_of: [1, 2] }, - value: 3, - '*': { __one_of: [4, 5] }, - }, - }, - }, - }, - 'GET _test', - [ - { - name: 'not matching object when { is not opened', - cursor: { lineNumber: 2, column: 13 }, - initialValue: '', - autoCompleteSet: ['{'], - }, - { - name: 'not matching array when [ is not opened', - cursor: { lineNumber: 3, column: 13 }, - initialValue: '', - autoCompleteSet: ['['], - }, - { - name: 'matching value with one_of', - cursor: { lineNumber: 4, column: 20 }, - initialValue: '', - autoCompleteSet: [1, 2], - }, - { - name: 'matching value', - cursor: { lineNumber: 5, column: 13 }, - initialValue: '', - autoCompleteSet: [3], - }, - { - name: 'matching any value with one_of', - cursor: { lineNumber: 6, column: 22 }, - initialValue: '', - autoCompleteSet: [4, 5], - }, - ] - ); - - contextTests( - { - query: { - field: 'something', - }, - facets: { - name: {}, - }, - size: 20, - }, - MAPPING, - SEARCH_KB, - 'GET _search', - [ - { - name: '* matching everything', - cursor: { lineNumber: 6, column: 16 }, - initialValue: '', - addTemplate: true, - prefixToAdd: '', - suffixToAdd: '', - rangeToReplace: { - start: { lineNumber: 6, column: 16 }, - end: { lineNumber: 6, column: 16 }, - }, - autoCompleteSet: [{ name: 'terms', meta: 'API' }], - }, - ] - ); - - contextTests( - { - index: '123', - }, - MAPPING, - { - endpoints: { - _test: { - patterns: ['_test'], - data_autocomplete_rules: { - index: '{index}', - }, - }, - }, - }, - 'GET _test', - [ - { - name: '{index} matching', - cursor: { lineNumber: 2, column: 16 }, - autoCompleteSet: [ - { name: 'index1', meta: 'index' }, - { name: 'index2', meta: 'index' }, - ], - }, - ] - ); - - function tt(term, template, meta) { - term = { name: term, template: template }; - if (meta) { - term.meta = meta; - } - return term; - } - - contextTests( - { - array: ['a'], - oneof: '1', - }, - MAPPING, - { - endpoints: { - _endpoint: { - patterns: ['_endpoint'], - data_autocomplete_rules: { - array: ['a', 'b'], - number: 1, - object: {}, - fixed: { __template: { a: 1 } }, - oneof: { __one_of: ['o1', 'o2'] }, - }, - }, - }, - }, - 'GET _endpoint', - [ - { - name: 'Templates 1', - cursor: { lineNumber: 2, column: 1 }, - autoCompleteSet: [ - tt('array', []), - tt('fixed', { a: 1 }), - tt('number', 1), - tt('object', {}), - tt('oneof', 'o1'), - ], - }, - { - name: 'Templates - one off', - cursor: { lineNumber: 5, column: 13 }, - autoCompleteSet: [tt('o1'), tt('o2')], - }, - ] - ); - - contextTests( - { - string: 'value', - context: {}, - }, - MAPPING, - { - endpoints: { - _endpoint: { - patterns: ['_endpoint'], - data_autocomplete_rules: { - context: { - __one_of: [ - { - __condition: { - lines_regex: 'value', - }, - match: {}, - }, - { - __condition: { - lines_regex: 'other', - }, - no_match: {}, - }, - { always: {} }, - ], - }, - }, - }, - }, - }, - 'GET _endpoint', - [ - { - name: 'Conditionals', - cursor: { lineNumber: 3, column: 16 }, - autoCompleteSet: [tt('always', {}), tt('match', {})], - }, - ] - ); - - contextTests( - { - any_of_numbers: [1], - any_of_obj: [ - { - a: 1, - }, - ], - any_of_mixed: [ - { - a: 1, - }, - 2, - ], - }, - MAPPING, - { - endpoints: { - _endpoint: { - patterns: ['_endpoint'], - data_autocomplete_rules: { - any_of_numbers: { __template: [1, 2], __any_of: [1, 2, 3] }, - any_of_obj: { - __template: [{ c: 1 }], - __any_of: [{ a: 1, b: 2 }, { c: 1 }], - }, - any_of_mixed: { - __any_of: [{ a: 1 }, 3], - }, - }, - }, - }, - }, - 'GET _endpoint', - [ - { - name: 'Any of - templates', - cursor: { lineNumber: 2, column: 1 }, - autoCompleteSet: [ - tt('any_of_mixed', []), - tt('any_of_numbers', [1, 2]), - tt('any_of_obj', [{ c: 1 }]), - ], - }, - { - name: 'Any of - numbers', - cursor: { lineNumber: 3, column: 3 }, - autoCompleteSet: [1, 2, 3], - }, - { - name: 'Any of - object', - cursor: { lineNumber: 7, column: 3 }, - autoCompleteSet: [tt('a', 1), tt('b', 2), tt('c', 1)], - }, - { - name: 'Any of - mixed - obj', - cursor: { lineNumber: 12, column: 3 }, - autoCompleteSet: [tt('a', 1)], - }, - { - name: 'Any of - mixed - both', - cursor: { lineNumber: 14, column: 3 }, - autoCompleteSet: [tt(3), tt('{')], - }, - ] - ); - - contextTests( - {}, - MAPPING, - { - endpoints: { - _endpoint: { - patterns: ['_endpoint'], - data_autocomplete_rules: { - query: '', - }, - }, - }, - }, - 'GET _endpoint', - [ - { - name: 'Empty string as default', - cursor: { lineNumber: 1, column: 2 }, - autoCompleteSet: [tt('query', '')], - }, - ] - ); - - // NOTE: This test emits "error while getting completion terms Error: failed to resolve link - // [GLOBAL.broken]: Error: failed to resolve global components for ['broken']". but that's - // expected. - contextTests( - { - a: { - b: {}, - c: {}, - d: { - t1a: {}, - }, - e: {}, - f: [{}], - g: {}, - h: {}, - }, - }, - MAPPING, - { - globals: { - gtarget: { - t1: 2, - t1a: { - __scope_link: '.', - }, - }, - }, - endpoints: { - _current: { - patterns: ['_current'], - data_autocomplete_rules: { - a: { - b: { - __scope_link: '.a', - }, - c: { - __scope_link: 'ext.target', - }, - d: { - __scope_link: 'GLOBAL.gtarget', - }, - e: { - __scope_link: 'ext', - }, - f: [ - { - __scope_link: 'ext.target', - }, - ], - g: { - __scope_link: function () { - return { - a: 1, - b: 2, - }; - }, - }, - h: { - __scope_link: 'GLOBAL.broken', - }, - }, - }, - }, - ext: { - patterns: ['ext'], - data_autocomplete_rules: { - target: { - t2: 1, - }, - }, - }, - }, - }, - 'GET _current', - [ - { - name: 'Relative scope link test', - cursor: { lineNumber: 3, column: 13 }, - autoCompleteSet: [ - tt('b', {}), - tt('c', {}), - tt('d', {}), - tt('e', {}), - tt('f', [{}]), - tt('g', {}), - tt('h', {}), - ], - }, - { - name: 'External scope link test', - cursor: { lineNumber: 4, column: 13 }, - autoCompleteSet: [tt('t2', 1)], - }, - { - name: 'Global scope link test', - cursor: { lineNumber: 5, column: 13 }, - autoCompleteSet: [tt('t1', 2), tt('t1a', {})], - }, - { - name: 'Global scope link with an internal scope link', - cursor: { lineNumber: 6, column: 18 }, - autoCompleteSet: [tt('t1', 2), tt('t1a', {})], - }, - { - name: 'Entire endpoint scope link test', - cursor: { lineNumber: 8, column: 13 }, - autoCompleteSet: [tt('target', {})], - }, - { - name: 'A scope link within an array', - cursor: { lineNumber: 10, column: 11 }, - autoCompleteSet: [tt('t2', 1)], - }, - { - name: 'A function based scope link', - cursor: { lineNumber: 12, column: 13 }, - autoCompleteSet: [tt('a', 1), tt('b', 2)], - }, - { - name: 'A global scope link with wrong link', - cursor: { lineNumber: 13, column: 13 }, - assertThrows: /broken/, - }, - ] - ); - - contextTests( - {}, - MAPPING, - { - globals: { - gtarget: { - t1: 2, - }, - }, - endpoints: { - _current: { - patterns: ['_current'], - id: 'GET _current', - data_autocomplete_rules: { - __scope_link: 'GLOBAL.gtarget', - }, - }, - }, - }, - 'GET _current', - [ - { - name: 'Top level scope link', - cursor: { lineNumber: 1, column: 2 }, - autoCompleteSet: [tt('t1', 2)], - }, - ] - ); - - contextTests( - { - a: {}, - }, - MAPPING, - { - endpoints: { - _endpoint: { - patterns: ['_endpoint'], - data_autocomplete_rules: { - a: {}, - b: {}, - }, - }, - }, - }, - 'GET _endpoint', - [ - { - name: 'Path after empty object', - cursor: { lineNumber: 2, column: 11 }, - autoCompleteSet: ['a', 'b'], - }, - ] - ); - - contextTests( - { - '': {}, - }, - MAPPING, - SEARCH_KB, - 'POST _search', - [ - { - name: 'Replace an empty string', - cursor: { lineNumber: 2, column: 5 }, - rangeToReplace: { - start: { lineNumber: 2, column: 4 }, - end: { lineNumber: 2, column: 10 }, - }, - }, - ] - ); - - contextTests( - { - a: [ - { - c: {}, - }, - ], - }, - MAPPING, - { - endpoints: { - _endpoint: { - patterns: ['_endpoint'], - data_autocomplete_rules: { - a: [{ b: 1 }], - }, - }, - }, - }, - 'GET _endpoint', - [ - { - name: 'List of objects - internal autocomplete', - cursor: { lineNumber: 4, column: 11 }, - autoCompleteSet: ['b'], - }, - { - name: 'List of objects - external template', - cursor: { lineNumber: 1, column: 2 }, - autoCompleteSet: [tt('a', [{}])], - }, - ] - ); - - contextTests( - { - query: { - term: { - field: 'something', - }, - }, - facets: { - test: { - terms: { - field: 'test', - }, - }, - }, - size: 20, - }, - MAPPING, - SEARCH_KB, - 'POST index1/_search', - [ - { - name: 'Field completion as scope', - cursor: { lineNumber: 4, column: 11 }, - autoCompleteSet: [ - tt('field1.1.1', { f: 1 }, 'string'), - tt('field1.1.2', { f: 1 }, 'string'), - ], - }, - { - name: 'Field completion as value', - cursor: { lineNumber: 10, column: 24 }, - autoCompleteSet: [ - { name: 'field1.1.1', meta: 'string' }, - { name: 'field1.1.2', meta: 'string' }, - ], - }, - ] - ); - - // NOTE: This test emits "Can't extract a valid url token path", but that's expected. - contextTests('POST _search\n', MAPPING, SEARCH_KB, null, [ - { - name: 'initial doc start', - cursor: { lineNumber: 2, column: 1 }, - autoCompleteSet: ['{'], - prefixToAdd: '', - suffixToAdd: '', - }, - ]); - - contextTests( - '{\n' + ' "query": {} \n' + '}\n' + '\n' + '\n', - MAPPING, - SEARCH_KB, - 'POST _search', - [ - { - name: 'Cursor rows after request end', - cursor: { lineNumber: 5, column: 1 }, - autoCompleteSet: ['GET', 'PUT', 'POST', 'DELETE', 'HEAD', 'PATCH'], - prefixToAdd: '', - suffixToAdd: ' ', - }, - { - name: 'Cursor just after request end', - cursor: { lineNumber: 3, column: 2 }, - no_context: true, - }, - ] - ); - - const CLUSTER_KB = { - endpoints: { - _search: { - patterns: ['_search', '{index}/_search'], - url_params: { - search_type: ['count', 'query_then_fetch'], - scroll: '10m', - }, - methods: ['GET'], - data_autocomplete_rules: {}, - }, - '_cluster/stats': { - patterns: ['_cluster/stats'], - indices_mode: 'none', - data_autocomplete_rules: {}, - methods: ['GET'], - }, - '_cluster/nodes/stats': { - patterns: ['_cluster/nodes/stats'], - data_autocomplete_rules: {}, - methods: ['GET'], - }, - }, - }; - - contextTests(null, MAPPING, CLUSTER_KB, 'GET _cluster', [ - { - name: 'Endpoints with slashes - no slash', - cursor: { lineNumber: 1, column: 9 }, - autoCompleteSet: ['_cluster/nodes/stats', '_cluster/stats', '_search', 'index1', 'index2'], - prefixToAdd: '', - suffixToAdd: '', - }, - ]); - - contextTests(null, MAPPING, CLUSTER_KB, 'GET _cluster/', [ - { - name: 'Endpoints with slashes - before slash', - cursor: { lineNumber: 1, column: 8 }, - autoCompleteSet: ['_cluster/nodes/stats', '_cluster/stats', '_search', 'index1', 'index2'], - prefixToAdd: '', - suffixToAdd: '', - }, - { - name: 'Endpoints with slashes - on slash', - cursor: { lineNumber: 1, column: 13 }, - autoCompleteSet: ['_cluster/nodes/stats', '_cluster/stats', '_search', 'index1', 'index2'], - prefixToAdd: '', - suffixToAdd: '', - }, - { - name: 'Endpoints with slashes - after slash', - cursor: { lineNumber: 1, column: 14 }, - autoCompleteSet: ['nodes/stats', 'stats'], - prefixToAdd: '', - suffixToAdd: '', - }, - ]); - - contextTests(null, MAPPING, CLUSTER_KB, 'GET _cluster/no', [ - { - name: 'Endpoints with slashes - after slash', - cursor: { lineNumber: 1, column: 15 }, - autoCompleteSet: [ - { name: 'nodes/stats', meta: 'endpoint' }, - { name: 'stats', meta: 'endpoint' }, - ], - prefixToAdd: '', - suffixToAdd: '', - initialValue: 'no', - }, - ]); - - contextTests(null, MAPPING, CLUSTER_KB, 'GET _cluster/nodes/st', [ - { - name: 'Endpoints with two slashes', - cursor: { lineNumber: 1, column: 21 }, - autoCompleteSet: ['stats'], - prefixToAdd: '', - suffixToAdd: '', - initialValue: 'st', - }, - ]); - - contextTests(null, MAPPING, CLUSTER_KB, 'GET ', [ - { - name: 'Immediately after space + method', - cursor: { lineNumber: 1, column: 5 }, - autoCompleteSet: [ - { name: '_cluster/nodes/stats', meta: 'endpoint' }, - { name: '_cluster/stats', meta: 'endpoint' }, - { name: '_search', meta: 'endpoint' }, - { name: 'index1', meta: 'index' }, - { name: 'index2', meta: 'index' }, - ], - prefixToAdd: '', - suffixToAdd: '', - initialValue: '', - }, - ]); - - contextTests(null, MAPPING, CLUSTER_KB, 'GET cl', [ - { - name: 'Endpoints by subpart GET', - cursor: { lineNumber: 1, column: 7 }, - autoCompleteSet: [ - { name: '_cluster/nodes/stats', meta: 'endpoint' }, - { name: '_cluster/stats', meta: 'endpoint' }, - { name: '_search', meta: 'endpoint' }, - { name: 'index1', meta: 'index' }, - { name: 'index2', meta: 'index' }, - ], - prefixToAdd: '', - suffixToAdd: '', - initialValue: 'cl', - method: 'GET', - }, - ]); - - contextTests(null, MAPPING, CLUSTER_KB, 'POST cl', [ - { - name: 'Endpoints by subpart POST', - cursor: { lineNumber: 1, column: 8 }, - no_context: true, - prefixToAdd: '', - suffixToAdd: '', - initialValue: 'cl', - }, - ]); - - contextTests(null, MAPPING, CLUSTER_KB, 'GET _search?', [ - { - name: 'Params just after ?', - cursor: { lineNumber: 1, column: 13 }, - autoCompleteSet: [ - { name: 'filter_path', meta: 'param', insertValue: 'filter_path=' }, - { name: 'format', meta: 'param', insertValue: 'format=' }, - { name: 'pretty', meta: 'flag' }, - { name: 'scroll', meta: 'param', insertValue: 'scroll=' }, - { name: 'search_type', meta: 'param', insertValue: 'search_type=' }, - ], - prefixToAdd: '', - suffixToAdd: '', - }, - ]); - - contextTests(null, MAPPING, CLUSTER_KB, 'GET _search?format=', [ - { - name: 'Params values', - cursor: { lineNumber: 1, column: 20 }, - autoCompleteSet: [ - { name: 'json', meta: 'format' }, - { name: 'yaml', meta: 'format' }, - ], - prefixToAdd: '', - suffixToAdd: '', - }, - ]); - - contextTests(null, MAPPING, CLUSTER_KB, 'GET _search?format=yaml&', [ - { - name: 'Params after amp', - cursor: { lineNumber: 1, column: 25 }, - autoCompleteSet: [ - { name: 'filter_path', meta: 'param', insertValue: 'filter_path=' }, - { name: 'format', meta: 'param', insertValue: 'format=' }, - { name: 'pretty', meta: 'flag' }, - { name: 'scroll', meta: 'param', insertValue: 'scroll=' }, - { name: 'search_type', meta: 'param', insertValue: 'search_type=' }, - ], - prefixToAdd: '', - suffixToAdd: '', - }, - ]); - - contextTests(null, MAPPING, CLUSTER_KB, 'GET _search?format=yaml&search', [ - { - name: 'Params on existing param', - cursor: { lineNumber: 1, column: 27 }, - rangeToReplace: { - start: { lineNumber: 1, column: 25 }, - end: { lineNumber: 1, column: 31 }, - }, - autoCompleteSet: [ - { name: 'filter_path', meta: 'param', insertValue: 'filter_path=' }, - { name: 'format', meta: 'param', insertValue: 'format=' }, - { name: 'pretty', meta: 'flag' }, - { name: 'scroll', meta: 'param', insertValue: 'scroll=' }, - { name: 'search_type', meta: 'param', insertValue: 'search_type=' }, - ], - prefixToAdd: '', - suffixToAdd: '', - }, - ]); - - contextTests(null, MAPPING, CLUSTER_KB, 'GET _search?format=yaml&search_type=cou', [ - { - name: 'Params on existing value', - cursor: { lineNumber: 1, column: 38 }, - rangeToReplace: { - start: { lineNumber: 1, column: 37 }, - end: { lineNumber: 1, column: 40 }, - }, - autoCompleteSet: [ - { name: 'count', meta: 'search_type' }, - { name: 'query_then_fetch', meta: 'search_type' }, - ], - prefixToAdd: '', - suffixToAdd: '', - }, - ]); - - contextTests(null, MAPPING, CLUSTER_KB, 'GET _search?format=yaml&search_type=cou', [ - { - name: 'Params on just after = with existing value', - cursor: { lineNumber: 1, column: 37 }, - rangeToReplace: { - start: { lineNumber: 1, column: 37 }, - end: { lineNumber: 1, column: 37 }, - }, - autoCompleteSet: [ - { name: 'count', meta: 'search_type' }, - { name: 'query_then_fetch', meta: 'search_type' }, - ], - prefixToAdd: '', - suffixToAdd: '', - }, - ]); - - contextTests( - { - query: { - field: 'something', - }, - facets: {}, - size: 20, - }, - MAPPING, - SEARCH_KB, - 'POST http://somehost/_search', - [ - { - name: 'fullurl - existing dictionary key, no template', - cursor: { lineNumber: 2, column: 7 }, - initialValue: 'query', - addTemplate: false, - prefixToAdd: '', - suffixToAdd: '', - rangeToReplace: { - start: { lineNumber: 2, column: 4 }, - end: { lineNumber: 2, column: 11 }, - }, - autoCompleteSet: ['facets', 'query', 'size'], - }, - { - name: 'fullurl - existing inner dictionary key', - cursor: { lineNumber: 3, column: 8 }, - initialValue: 'field', - addTemplate: false, - prefixToAdd: '', - suffixToAdd: '', - rangeToReplace: { - start: { lineNumber: 3, column: 7 }, - end: { lineNumber: 3, column: 14 }, - }, - autoCompleteSet: ['match_all', 'term'], - }, - { - name: 'fullurl - existing dictionary key, yes template', - cursor: { lineNumber: 5, column: 8 }, - initialValue: 'facets', - addTemplate: true, - prefixToAdd: '', - suffixToAdd: '', - rangeToReplace: { - start: { lineNumber: 5, column: 4 }, - end: { lineNumber: 5, column: 16 }, - }, - autoCompleteSet: ['facets', 'query', 'size'], - }, - ] - ); -}); 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 deleted file mode 100644 index 19d782f1b8e87..0000000000000 --- a/src/plugins/console/public/application/models/sense_editor/sense_editor.test.js +++ /dev/null @@ -1,641 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the "Elastic License - * 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side - * Public License v 1"; you may not use this file except in compliance with, at - * your election, the "Elastic License 2.0", the "GNU Affero General Public - * License v3.0 only", or the "Server Side Public License, v 1". - */ - -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'; -import editorInput1 from './__fixtures__/editor_input1.txt'; -import { setStorage, createStorage } from '../../../services'; - -const { collapseLiteralStrings } = XJson; - -describe('Editor', () => { - let input; - let oldUrl; - let olldWindow; - let storage; - - beforeEach(function () { - // Set up our document body - document.body.innerHTML = `
-
-
-
-
`; - - input = create(document.querySelector('#ConAppEditor')); - $(input.getCoreEditor().getContainer()).show(); - input.autocomplete._test.removeChangeListener(); - oldUrl = global.URL; - olldWindow = { ...global.window }; - global.URL = URL; - Object.defineProperty(global, 'window', { - value: Object.create(window), - writable: true, - }); - Object.defineProperty(window, 'location', { - value: { - origin: 'http://localhost:5620', - }, - }); - storage = createStorage({ - engine: global.window.localStorage, - prefix: 'console_test', - }); - setStorage(storage); - }); - afterEach(function () { - global.URL = oldUrl; - global.window = olldWindow; - $(input.getCoreEditor().getContainer()).hide(); - input.autocomplete._test.addChangeListener(); - setStorage(null); - }); - - let testCount = 0; - - const callWithEditorMethod = (editorMethod, fn) => async (done) => { - const results = await input[editorMethod](); - fn(results, done); - }; - - function utilsTest(name, prefix, data, testToRun) { - const id = testCount++; - if (typeof data === 'function') { - testToRun = data; - data = null; - } - if (data && typeof data !== 'string') { - data = JSON.stringify(data, null, 3); - } - if (data) { - if (prefix) { - data = prefix + '\n' + data; - } - } else { - data = prefix; - } - - test('Utils test ' + id + ' : ' + name, function (done) { - input.update(data, true).then(() => { - testToRun(done); - }); - }); - } - - function compareRequest(requests, expected) { - if (!Array.isArray(requests)) { - requests = [requests]; - expected = [expected]; - } - - _.each(requests, function (r) { - delete r.range; - }); - expect(requests).toEqual(expected); - } - - const simpleRequest = { - prefix: 'POST _search', - data: ['{', ' "query": { "match_all": {} }', '}'].join('\n'), - }; - - const singleLineRequest = { - prefix: 'POST _search', - data: '{ "query": { "match_all": {} } }', - }; - - const getRequestNoData = { - prefix: 'GET _stats', - }; - - const multiDocRequest = { - prefix: 'POST _bulk', - data_as_array: ['{ "index": { "_index": "index", "_type":"type" } }', '{ "field": 1 }'], - }; - multiDocRequest.data = multiDocRequest.data_as_array.join('\n'); - - utilsTest( - 'simple request range', - simpleRequest.prefix, - simpleRequest.data, - callWithEditorMethod('getRequestRange', (range, done) => { - compareRequest(range, { - start: { lineNumber: 1, column: 1 }, - end: { lineNumber: 4, column: 2 }, - }); - done(); - }) - ); - - utilsTest( - 'simple request data', - simpleRequest.prefix, - simpleRequest.data, - callWithEditorMethod('getRequest', (request, done) => { - const expected = { - method: 'POST', - url: '_search', - data: [simpleRequest.data], - }; - compareRequest(request, expected); - done(); - }) - ); - - utilsTest( - 'simple request range, prefixed with spaces', - ' ' + simpleRequest.prefix, - simpleRequest.data, - callWithEditorMethod('getRequestRange', (range, done) => { - expect(range).toEqual({ - start: { lineNumber: 1, column: 1 }, - end: { lineNumber: 4, column: 2 }, - }); - done(); - }) - ); - - utilsTest( - 'simple request data, prefixed with spaces', - ' ' + simpleRequest.prefix, - simpleRequest.data, - callWithEditorMethod('getRequest', (request, done) => { - const expected = { - method: 'POST', - url: '_search', - data: [simpleRequest.data], - }; - - compareRequest(request, expected); - done(); - }) - ); - - utilsTest( - 'simple request range, suffixed with spaces', - simpleRequest.prefix + ' ', - simpleRequest.data + ' ', - callWithEditorMethod('getRequestRange', (range, done) => { - compareRequest(range, { - start: { lineNumber: 1, column: 1 }, - end: { lineNumber: 4, column: 2 }, - }); - done(); - }) - ); - - utilsTest( - 'simple request data, suffixed with spaces', - simpleRequest.prefix + ' ', - simpleRequest.data + ' ', - callWithEditorMethod('getRequest', (request, done) => { - const expected = { - method: 'POST', - url: '_search', - data: [simpleRequest.data], - }; - - compareRequest(request, expected); - done(); - }) - ); - - utilsTest( - 'single line request range', - singleLineRequest.prefix, - singleLineRequest.data, - callWithEditorMethod('getRequestRange', (range, done) => { - compareRequest(range, { - start: { lineNumber: 1, column: 1 }, - end: { lineNumber: 2, column: 33 }, - }); - done(); - }) - ); - - utilsTest( - 'full url: single line request data', - 'POST https://somehost/_search', - singleLineRequest.data, - callWithEditorMethod('getRequest', (request, done) => { - const expected = { - method: 'POST', - url: 'https://somehost/_search', - data: [singleLineRequest.data], - }; - compareRequest(request, expected); - done(); - }) - ); - - utilsTest( - 'request with no data followed by a new line', - getRequestNoData.prefix, - '\n', - callWithEditorMethod('getRequestRange', (range, done) => { - compareRequest(range, { - start: { lineNumber: 1, column: 1 }, - end: { lineNumber: 1, column: 11 }, - }); - done(); - }) - ); - - utilsTest( - 'request with no data followed by a new line (data)', - getRequestNoData.prefix, - '\n', - callWithEditorMethod('getRequest', (request, done) => { - const expected = { - method: 'GET', - url: '_stats', - data: [], - }; - compareRequest(request, expected); - done(); - }) - ); - - utilsTest( - 'request with no data', - getRequestNoData.prefix, - getRequestNoData.data, - callWithEditorMethod('getRequestRange', (range, done) => { - expect(range).toEqual({ - start: { lineNumber: 1, column: 1 }, - end: { lineNumber: 1, column: 11 }, - }); - done(); - }) - ); - - utilsTest( - 'request with no data (data)', - getRequestNoData.prefix, - getRequestNoData.data, - callWithEditorMethod('getRequest', (request, done) => { - const expected = { - method: 'GET', - url: '_stats', - data: [], - }; - compareRequest(request, expected); - done(); - }) - ); - - utilsTest( - 'multi doc request range', - multiDocRequest.prefix, - multiDocRequest.data, - callWithEditorMethod('getRequestRange', (range, done) => { - expect(range).toEqual({ - start: { lineNumber: 1, column: 1 }, - end: { lineNumber: 3, column: 15 }, - }); - done(); - }) - ); - - utilsTest( - 'multi doc request data', - multiDocRequest.prefix, - multiDocRequest.data, - callWithEditorMethod('getRequest', (request, done) => { - const expected = { - method: 'POST', - url: '_bulk', - data: multiDocRequest.data_as_array, - }; - compareRequest(request, expected); - done(); - }) - ); - - const scriptRequest = { - prefix: 'POST _search', - data: ['{', ' "query": { "script": """', ' some script ', ' """}', '}'].join('\n'), - }; - - utilsTest( - 'script request range', - scriptRequest.prefix, - scriptRequest.data, - callWithEditorMethod('getRequestRange', (range, done) => { - compareRequest(range, { - start: { lineNumber: 1, column: 1 }, - end: { lineNumber: 6, column: 2 }, - }); - done(); - }) - ); - - utilsTest( - 'simple request data', - simpleRequest.prefix, - simpleRequest.data, - callWithEditorMethod('getRequest', (request, done) => { - const expected = { - method: 'POST', - url: '_search', - data: [collapseLiteralStrings(simpleRequest.data)], - }; - - compareRequest(request, expected); - done(); - }) - ); - - function multiReqTest(name, editorInput, range, expected) { - utilsTest('multi request select - ' + name, editorInput, async function (done) { - const requests = await input.getRequestsInRange(range, false); - // convert to format returned by request. - _.each(expected, function (req) { - req.data = req.data == null ? [] : [JSON.stringify(req.data, null, 2)]; - }); - - compareRequest(requests, expected); - done(); - }); - } - - multiReqTest( - 'mid body to mid body', - editorInput1, - { start: { lineNumber: 13 }, end: { lineNumber: 18 } }, - [ - { - method: 'PUT', - url: 'index_1/type1/1', - data: { - f: 1, - }, - }, - { - method: 'PUT', - url: 'index_1/type1/2', - data: { - f: 2, - }, - }, - ] - ); - - multiReqTest( - 'single request start to end', - editorInput1, - { start: { lineNumber: 11 }, end: { lineNumber: 14 } }, - [ - { - method: 'PUT', - url: 'index_1/type1/1', - data: { - f: 1, - }, - }, - ] - ); - - multiReqTest( - 'start to end, with comment', - editorInput1, - { start: { lineNumber: 7 }, end: { lineNumber: 14 } }, - [ - { - method: 'GET', - url: '_stats?level=shards', - data: null, - }, - { - method: 'PUT', - url: 'index_1/type1/1', - data: { - f: 1, - }, - }, - ] - ); - - multiReqTest( - 'before start to after end, with comments', - editorInput1, - { start: { lineNumber: 5 }, end: { lineNumber: 15 } }, - [ - { - method: 'GET', - url: '_stats?level=shards', - data: null, - }, - { - method: 'PUT', - url: 'index_1/type1/1', - data: { - f: 1, - }, - }, - ] - ); - - multiReqTest( - 'between requests', - editorInput1, - { start: { lineNumber: 22 }, end: { lineNumber: 23 } }, - [] - ); - - multiReqTest( - 'between requests - with comment', - editorInput1, - { start: { lineNumber: 21 }, end: { lineNumber: 23 } }, - [] - ); - - multiReqTest( - 'between requests - before comment', - editorInput1, - { start: { lineNumber: 20 }, end: { lineNumber: 23 } }, - [] - ); - - function multiReqCopyAsCurlTest(name, editorInput, range, expected) { - utilsTest('multi request copy as curl - ' + name, editorInput, async function (done) { - const curl = await input.getRequestsAsCURL('http://localhost:9200', range); - expect(curl).toEqual(expected); - done(); - }); - } - - multiReqCopyAsCurlTest( - 'start to end, with comment', - editorInput1, - { start: { lineNumber: 7 }, end: { lineNumber: 14 } }, - ` -curl -XGET "http://localhost:9200/_stats?level=shards" -H "kbn-xsrf: reporting" - -#in between comment - -curl -XPUT "http://localhost:9200/index_1/type1/1" -H "kbn-xsrf: reporting" -H "Content-Type: application/json" -d' -{ - "f": 1 -}'`.trim() - ); - - multiReqCopyAsCurlTest( - 'with single quotes', - editorInput1, - { start: { lineNumber: 29 }, end: { lineNumber: 33 } }, - ` -curl -XPOST "http://localhost:9200/_sql?format=txt" -H "kbn-xsrf: reporting" -H "Content-Type: application/json" -d' -{ - "query": "SELECT prenom FROM claude_index WHERE prenom = '\\''claude'\\'' ", - "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() - ); - - describe('getRequestsAsCURL', () => { - it('should return empty string if no requests', async () => { - input?.getCoreEditor().setValue('', false); - const curl = await input.getRequestsAsCURL('http://localhost:9200', { - start: { lineNumber: 1 }, - end: { lineNumber: 1 }, - }); - expect(curl).toEqual(''); - }); - - it('should replace variables in the URL', async () => { - storage.set('variables', [{ name: 'exampleVariableA', value: 'valueA' }]); - input?.getCoreEditor().setValue('GET ${exampleVariableA}', false); - const curl = await input.getRequestsAsCURL('http://localhost:9200', { - start: { lineNumber: 1 }, - end: { lineNumber: 1 }, - }); - expect(curl).toContain('valueA'); - }); - - it('should replace variables in the body', async () => { - storage.set('variables', [{ name: 'exampleVariableB', value: 'valueB' }]); - console.log(storage.get('variables')); - input - ?.getCoreEditor() - .setValue('GET _search\n{\t\t"query": {\n\t\t\t"${exampleVariableB}": ""\n\t}\n}', false); - const curl = await input.getRequestsAsCURL('http://localhost:9200', { - start: { lineNumber: 1 }, - end: { lineNumber: 6 }, - }); - expect(curl).toContain('valueB'); - }); - - it('should strip comments in the URL', async () => { - input?.getCoreEditor().setValue('GET _search // comment', false); - const curl = await input.getRequestsAsCURL('http://localhost:9200', { - start: { lineNumber: 1 }, - end: { lineNumber: 6 }, - }); - expect(curl).not.toContain('comment'); - }); - - it('should strip comments in the body', async () => { - input - ?.getCoreEditor() - .setValue('{\n\t"query": {\n\t\t"match_all": {} // comment \n\t}\n}', false); - const curl = await input.getRequestsAsCURL('http://localhost:9200', { - start: { lineNumber: 1 }, - end: { lineNumber: 8 }, - }); - console.log('curl', curl); - expect(curl).not.toContain('comment'); - }); - - it('should strip multi-line comments in the body', async () => { - input - ?.getCoreEditor() - .setValue('{\n\t"query": {\n\t\t"match_all": {} /* comment */\n\t}\n}', false); - const curl = await input.getRequestsAsCURL('http://localhost:9200', { - start: { lineNumber: 1 }, - end: { lineNumber: 8 }, - }); - console.log('curl', curl); - expect(curl).not.toContain('comment'); - }); - - it('should replace multiple variables in the URL', async () => { - storage.set('variables', [ - { name: 'exampleVariableA', value: 'valueA' }, - { name: 'exampleVariableB', value: 'valueB' }, - ]); - input?.getCoreEditor().setValue('GET ${exampleVariableA}/${exampleVariableB}', false); - const curl = await input.getRequestsAsCURL('http://localhost:9200', { - start: { lineNumber: 1 }, - end: { lineNumber: 1 }, - }); - expect(curl).toContain('valueA'); - expect(curl).toContain('valueB'); - }); - - it('should replace multiple variables in the body', async () => { - storage.set('variables', [ - { name: 'exampleVariableA', value: 'valueA' }, - { name: 'exampleVariableB', value: 'valueB' }, - ]); - input - ?.getCoreEditor() - .setValue( - 'GET _search\n{\t\t"query": {\n\t\t\t"${exampleVariableA}": "${exampleVariableB}"\n\t}\n}', - false - ); - const curl = await input.getRequestsAsCURL('http://localhost:9200', { - start: { lineNumber: 1 }, - end: { lineNumber: 6 }, - }); - expect(curl).toContain('valueA'); - expect(curl).toContain('valueB'); - }); - - it('should replace variables in bulk request', async () => { - storage.set('variables', [ - { name: 'exampleVariableA', value: 'valueA' }, - { name: 'exampleVariableB', value: 'valueB' }, - ]); - input - ?.getCoreEditor() - .setValue( - 'POST _bulk\n{"index": {"_id": "0"}}\n{"field" : "${exampleVariableA}"}\n{"index": {"_id": "1"}}\n{"field" : "${exampleVariableB}"}\n', - false - ); - const curl = await input.getRequestsAsCURL('http://localhost:9200', { - start: { lineNumber: 1 }, - end: { lineNumber: 4 }, - }); - expect(curl).toContain('valueA'); - expect(curl).toContain('valueB'); - }); - }); -}); diff --git a/src/plugins/console/public/application/models/sense_editor/sense_editor.test.mocks.ts b/src/plugins/console/public/application/models/sense_editor/sense_editor.test.mocks.ts deleted file mode 100644 index f0ec279fb4ffe..0000000000000 --- a/src/plugins/console/public/application/models/sense_editor/sense_editor.test.mocks.ts +++ /dev/null @@ -1,20 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the "Elastic License - * 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side - * Public License v 1"; you may not use this file except in compliance with, at - * your election, the "Elastic License 2.0", the "GNU Affero General Public - * License v3.0 only", or the "Server Side Public License, v 1". - */ - -/* eslint no-undef: 0 */ - -import '../legacy_core_editor/legacy_core_editor.test.mocks'; - -import jQuery from 'jquery'; -jest.spyOn(jQuery, 'ajax').mockImplementation( - () => - new Promise(() => { - // never resolve - }) as any -); diff --git a/src/plugins/console/public/application/models/sense_editor/sense_editor.ts b/src/plugins/console/public/application/models/sense_editor/sense_editor.ts deleted file mode 100644 index f6b0439cb283e..0000000000000 --- a/src/plugins/console/public/application/models/sense_editor/sense_editor.ts +++ /dev/null @@ -1,534 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the "Elastic License - * 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side - * Public License v 1"; you may not use this file except in compliance with, at - * your election, the "Elastic License 2.0", the "GNU Affero General Public - * License v3.0 only", or the "Server Side Public License, v 1". - */ - -import _ from 'lodash'; -import { parse } from 'hjson'; -import { XJson } from '@kbn/es-ui-shared-plugin/public'; - -import RowParser from '../../../lib/row_parser'; -import * as utils from '../../../lib/utils'; -import { constructUrl } from '../../../lib/es/es'; - -import { CoreEditor, Position, Range } from '../../../types'; -import { createTokenIterator } from '../../factories'; -import createAutocompleter from '../../../lib/autocomplete/autocomplete'; -import { getStorage, StorageKeys } from '../../../services'; -import { DEFAULT_VARIABLES } from '../../../../common/constants'; - -const { collapseLiteralStrings } = XJson; - -export class SenseEditor { - currentReqRange: (Range & { markerRef: unknown }) | null; - parser: RowParser; - - private readonly autocomplete: ReturnType; - - constructor(private readonly coreEditor: CoreEditor) { - this.currentReqRange = null; - this.parser = new RowParser(this.coreEditor); - this.autocomplete = createAutocompleter({ - coreEditor, - parser: this.parser, - }); - this.coreEditor.registerAutocompleter(this.autocomplete.getCompletions); - this.coreEditor.on( - 'tokenizerUpdate', - this.highlightCurrentRequestsAndUpdateActionBar.bind(this) - ); - this.coreEditor.on('changeCursor', this.highlightCurrentRequestsAndUpdateActionBar.bind(this)); - this.coreEditor.on('changeScrollTop', this.updateActionsBar.bind(this)); - } - - prevRequestStart = (rowOrPos?: number | Position): Position => { - let curRow: number; - - if (rowOrPos == null) { - curRow = this.coreEditor.getCurrentPosition().lineNumber; - } else if (_.isObject(rowOrPos)) { - curRow = (rowOrPos as Position).lineNumber; - } else { - curRow = rowOrPos as number; - } - - while (curRow > 0 && !this.parser.isStartRequestRow(curRow, this.coreEditor)) curRow--; - - return { - lineNumber: curRow, - column: 1, - }; - }; - - nextRequestStart = (rowOrPos?: number | Position) => { - let curRow: number; - if (rowOrPos == null) { - curRow = this.coreEditor.getCurrentPosition().lineNumber; - } else if (_.isObject(rowOrPos)) { - curRow = (rowOrPos as Position).lineNumber; - } else { - curRow = rowOrPos as number; - } - const maxLines = this.coreEditor.getLineCount(); - for (; curRow < maxLines - 1; curRow++) { - if (this.parser.isStartRequestRow(curRow, this.coreEditor)) { - break; - } - } - return { - row: curRow, - column: 0, - }; - }; - - autoIndent = _.debounce(async () => { - await this.coreEditor.waitForLatestTokens(); - const reqRange = await this.getRequestRange(); - if (!reqRange) { - return; - } - const parsedReq = await this.getRequest(); - - if (!parsedReq) { - return; - } - - if (parsedReq.data.some((doc) => utils.hasComments(doc))) { - /** - * Comments require different approach for indentation and do not have condensed format - * We need to delegate indentation logic to coreEditor since it has access to session and other methods used for formatting and indenting the comments - */ - this.coreEditor.autoIndent(parsedReq.range); - return; - } - - if (parsedReq.data && parsedReq.data.length > 0) { - let indent = parsedReq.data.length === 1; // unindent multi docs by default - let formattedData = utils.formatRequestBodyDoc(parsedReq.data, indent); - if (!formattedData.changed) { - // toggle. - indent = !indent; - formattedData = utils.formatRequestBodyDoc(parsedReq.data, indent); - } - parsedReq.data = formattedData.data; - - this.replaceRequestRange(parsedReq, reqRange); - } - }, 25); - - update = async (data: string, reTokenizeAll = false) => { - return this.coreEditor.setValue(data, reTokenizeAll); - }; - - replaceRequestRange = ( - newRequest: { method: string; url: string; data: string | string[] }, - requestRange: Range - ) => { - const text = utils.textFromRequest(newRequest); - if (requestRange) { - this.coreEditor.replaceRange(requestRange, text); - } else { - // just insert where we are - this.coreEditor.insert(this.coreEditor.getCurrentPosition(), text); - } - }; - - getRequestRange = async (lineNumber?: number): Promise => { - await this.coreEditor.waitForLatestTokens(); - - if (this.parser.isInBetweenRequestsRow(lineNumber)) { - return null; - } - - const reqStart = this.prevRequestStart(lineNumber); - const reqEnd = this.nextRequestEnd(reqStart); - - return { - start: { - ...reqStart, - }, - end: { - ...reqEnd, - }, - }; - }; - - expandRangeToRequestEdges = async ( - range = this.coreEditor.getSelectionRange() - ): Promise => { - await this.coreEditor.waitForLatestTokens(); - - let startLineNumber = range.start.lineNumber; - let endLineNumber = range.end.lineNumber; - const maxLine = Math.max(1, this.coreEditor.getLineCount()); - - if (this.parser.isInBetweenRequestsRow(startLineNumber)) { - /* Do nothing... */ - } else { - for (; startLineNumber >= 1; startLineNumber--) { - if (this.parser.isStartRequestRow(startLineNumber)) { - break; - } - } - } - - if (startLineNumber < 1 || startLineNumber > endLineNumber) { - return null; - } - // move end row to the previous request end if between requests, otherwise walk forward - if (this.parser.isInBetweenRequestsRow(endLineNumber)) { - for (; endLineNumber >= startLineNumber; endLineNumber--) { - if (this.parser.isEndRequestRow(endLineNumber)) { - break; - } - } - } else { - for (; endLineNumber <= maxLine; endLineNumber++) { - if (this.parser.isEndRequestRow(endLineNumber)) { - break; - } - } - } - - if (endLineNumber < startLineNumber || endLineNumber > maxLine) { - return null; - } - - const endColumn = - (this.coreEditor.getLineValue(endLineNumber) || '').replace(/\s+$/, '').length + 1; - return { - start: { - lineNumber: startLineNumber, - column: 1, - }, - end: { - lineNumber: endLineNumber, - column: endColumn, - }, - }; - }; - - getRequestInRange = async (range?: Range) => { - await this.coreEditor.waitForLatestTokens(); - if (!range) { - return null; - } - const request: { - method: string; - data: string[]; - url: string; - range: Range; - } = { - method: '', - data: [], - url: '', - range, - }; - - const pos = range.start; - const tokenIter = createTokenIterator({ editor: this.coreEditor, position: pos }); - let t = tokenIter.getCurrentToken(); - if (this.parser.isEmptyToken(t)) { - // if the row starts with some spaces, skip them. - t = this.parser.nextNonEmptyToken(tokenIter); - } - if (t == null) { - return null; - } - - request.method = t.value; - t = this.parser.nextNonEmptyToken(tokenIter); - - if (!t || t.type === 'method') { - return null; - } - - request.url = ''; - - while (t && t.type && (t.type.indexOf('url') === 0 || t.type === 'variable.template')) { - request.url += t.value; - t = tokenIter.stepForward(); - } - if (this.parser.isEmptyToken(t)) { - // if the url row ends with some spaces, skip them. - t = this.parser.nextNonEmptyToken(tokenIter); - } - - // If the url row ends with a comment, skip it - while (this.parser.isCommentToken(t)) { - t = tokenIter.stepForward(); - } - - let bodyStartLineNumber = (t ? 0 : 1) + tokenIter.getCurrentPosition().lineNumber; // artificially increase end of docs. - let dataEndPos: Position; - while ( - bodyStartLineNumber < range.end.lineNumber || - (bodyStartLineNumber === range.end.lineNumber && 1 < range.end.column) - ) { - dataEndPos = this.nextDataDocEnd({ - lineNumber: bodyStartLineNumber, - column: 1, - }); - const bodyRange: Range = { - start: { - lineNumber: bodyStartLineNumber, - column: 1, - }, - end: dataEndPos, - }; - const data = this.coreEditor.getValueInRange(bodyRange)!; - request.data.push(data.trim()); - bodyStartLineNumber = dataEndPos.lineNumber + 1; - } - - return request; - }; - - getRequestsInRange = async ( - range = this.coreEditor.getSelectionRange(), - includeNonRequestBlocks = false - ): Promise => { - await this.coreEditor.waitForLatestTokens(); - if (!range) { - return []; - } - - const expandedRange = await this.expandRangeToRequestEdges(range); - if (!expandedRange) { - return []; - } - - const requests: unknown[] = []; - - let rangeStartCursor = expandedRange.start.lineNumber; - const endLineNumber = expandedRange.end.lineNumber; - - // move to the next request start (during the second iterations this may not be exactly on a request - let currentLineNumber = expandedRange.start.lineNumber; - - const flushNonRequestBlock = () => { - if (includeNonRequestBlocks) { - const nonRequestPrefixBlock = this.coreEditor - .getLines(rangeStartCursor, currentLineNumber - 1) - .join('\n'); - if (nonRequestPrefixBlock) { - requests.push(nonRequestPrefixBlock); - } - } - }; - - while (currentLineNumber <= endLineNumber) { - if (this.parser.isStartRequestRow(currentLineNumber)) { - flushNonRequestBlock(); - const request = await this.getRequest(currentLineNumber); - if (!request) { - // Something has probably gone wrong. - return requests; - } else { - requests.push(request); - rangeStartCursor = currentLineNumber = request.range.end.lineNumber + 1; - } - } else { - ++currentLineNumber; - } - } - - flushNonRequestBlock(); - - return requests; - }; - - getRequest = async (row?: number) => { - await this.coreEditor.waitForLatestTokens(); - if (this.parser.isInBetweenRequestsRow(row)) { - return null; - } - - const range = await this.getRequestRange(row); - return this.getRequestInRange(range!); - }; - - moveToPreviousRequestEdge = async () => { - await this.coreEditor.waitForLatestTokens(); - const pos = this.coreEditor.getCurrentPosition(); - for ( - pos.lineNumber--; - pos.lineNumber > 1 && !this.parser.isRequestEdge(pos.lineNumber); - pos.lineNumber-- - ) { - // loop for side effects - } - this.coreEditor.moveCursorToPosition({ - lineNumber: pos.lineNumber, - column: 1, - }); - }; - - moveToNextRequestEdge = async (moveOnlyIfNotOnEdge: boolean) => { - await this.coreEditor.waitForLatestTokens(); - const pos = this.coreEditor.getCurrentPosition(); - const maxRow = this.coreEditor.getLineCount(); - if (!moveOnlyIfNotOnEdge) { - pos.lineNumber++; - } - for ( - ; - pos.lineNumber < maxRow && !this.parser.isRequestEdge(pos.lineNumber); - pos.lineNumber++ - ) { - // loop for side effects - } - this.coreEditor.moveCursorToPosition({ - lineNumber: pos.lineNumber, - column: 1, - }); - }; - - nextRequestEnd = (pos: Position): Position => { - pos = pos || this.coreEditor.getCurrentPosition(); - const maxLines = this.coreEditor.getLineCount(); - let curLineNumber = pos.lineNumber; - for (; curLineNumber <= maxLines; ++curLineNumber) { - const curRowMode = this.parser.getRowParseMode(curLineNumber); - // eslint-disable-next-line no-bitwise - if ((curRowMode & this.parser.MODE.REQUEST_END) > 0) { - break; - } - // eslint-disable-next-line no-bitwise - if (curLineNumber !== pos.lineNumber && (curRowMode & this.parser.MODE.REQUEST_START) > 0) { - break; - } - } - - const column = - (this.coreEditor.getLineValue(curLineNumber) || '').replace(/\s+$/, '').length + 1; - - return { - lineNumber: curLineNumber, - column, - }; - }; - - nextDataDocEnd = (pos: Position): Position => { - pos = pos || this.coreEditor.getCurrentPosition(); - let curLineNumber = pos.lineNumber; - const maxLines = this.coreEditor.getLineCount(); - for (; curLineNumber < maxLines; curLineNumber++) { - const curRowMode = this.parser.getRowParseMode(curLineNumber); - // eslint-disable-next-line no-bitwise - if ((curRowMode & this.parser.MODE.REQUEST_END) > 0) { - break; - } - // eslint-disable-next-line no-bitwise - if ((curRowMode & this.parser.MODE.MULTI_DOC_CUR_DOC_END) > 0) { - break; - } - // eslint-disable-next-line no-bitwise - if (curLineNumber !== pos.lineNumber && (curRowMode & this.parser.MODE.REQUEST_START) > 0) { - break; - } - } - - const column = - (this.coreEditor.getLineValue(curLineNumber) || '').length + - 1; /* Range goes to 1 after last char */ - - return { - lineNumber: curLineNumber, - column, - }; - }; - - highlightCurrentRequestsAndUpdateActionBar = _.debounce(async () => { - await this.coreEditor.waitForLatestTokens(); - const expandedRange = await this.expandRangeToRequestEdges(); - if (expandedRange === null && this.currentReqRange === null) { - return; - } - if ( - expandedRange !== null && - this.currentReqRange !== null && - expandedRange.start.lineNumber === this.currentReqRange.start.lineNumber && - expandedRange.end.lineNumber === this.currentReqRange.end.lineNumber - ) { - // same request, now see if we are on the first line and update the action bar - const cursorLineNumber = this.coreEditor.getCurrentPosition().lineNumber; - if (cursorLineNumber === this.currentReqRange.start.lineNumber) { - this.updateActionsBar(); - } - return; // nothing to do.. - } - - if (this.currentReqRange) { - this.coreEditor.removeMarker(this.currentReqRange.markerRef); - } - - this.currentReqRange = expandedRange as any; - if (this.currentReqRange) { - this.currentReqRange.markerRef = this.coreEditor.addMarker(this.currentReqRange); - } - this.updateActionsBar(); - }, 25); - - getRequestsAsCURL = async (elasticsearchBaseUrl: string, range?: Range): Promise => { - const variables = getStorage().get(StorageKeys.VARIABLES, DEFAULT_VARIABLES); - let requests = await this.getRequestsInRange(range, true); - requests = utils.replaceVariables(requests, variables); - const result = _.map(requests, (req) => { - if (typeof req === 'string') { - // no request block - return req; - } - - const path = req.url; - const method = req.method; - const data = req.data; - - // this is the first url defined in elasticsearch.hosts - const url = constructUrl(elasticsearchBaseUrl, path); - - // Append 'kbn-xsrf' header to bypass (XSRF/CSRF) protections - let ret = `curl -X${method.toUpperCase()} "${url}" -H "kbn-xsrf: reporting"`; - - if (data && data.length) { - const joinedData = data.join('\n'); - let dataAsString: string; - - try { - ret += ` -H "Content-Type: application/json" -d'\n`; - - if (utils.hasComments(joinedData)) { - // if there are comments in the data, we need to strip them out - const dataWithoutComments = parse(joinedData); - dataAsString = collapseLiteralStrings(JSON.stringify(dataWithoutComments, null, 2)); - } else { - dataAsString = collapseLiteralStrings(joinedData); - } - // We escape single quoted strings that are wrapped in single quoted strings - ret += dataAsString.replace(/'/g, "'\\''"); - if (data.length > 1) { - ret += '\n'; - } // end with a new line - ret += "'"; - } catch (e) { - throw new Error(`Error parsing data: ${e.message}`); - } - } - return ret; - }); - - return result.join('\n'); - }; - - updateActionsBar = () => { - return this.coreEditor.legacyUpdateUI(this.currentReqRange); - }; - - getCoreEditor() { - return this.coreEditor; - } -} diff --git a/src/plugins/console/public/application/stores/editor.ts b/src/plugins/console/public/application/stores/editor.ts index 556f4f64337e6..8ae24e5a422b7 100644 --- a/src/plugins/console/public/application/stores/editor.ts +++ b/src/plugins/console/public/application/stores/editor.ts @@ -12,7 +12,6 @@ import { produce } from 'immer'; import { identity } from 'fp-ts/lib/function'; import { DevToolsSettings, DEFAULT_SETTINGS } from '../../services'; import { TextObject } from '../../../common/text_object'; -import { SenseEditor } from '../models'; import { SHELL_TAB_ID } from '../containers/main/constants'; import { MonacoEditorActionsProvider } from '../containers/editor/monaco_editor_actions_provider'; import { RequestToRestore } from '../../types'; @@ -39,7 +38,7 @@ export const initialValue: Store = produce( ); export type Action = - | { type: 'setInputEditor'; payload: SenseEditor | MonacoEditorActionsProvider } + | { type: 'setInputEditor'; payload: MonacoEditorActionsProvider } | { type: 'setCurrentTextObject'; payload: TextObject } | { type: 'updateSettings'; payload: DevToolsSettings } | { type: 'setCurrentView'; payload: string } diff --git a/src/plugins/console/public/lib/ace_token_provider/token_provider.test.ts b/src/plugins/console/public/lib/ace_token_provider/token_provider.test.ts deleted file mode 100644 index b36d9855414bd..0000000000000 --- a/src/plugins/console/public/lib/ace_token_provider/token_provider.test.ts +++ /dev/null @@ -1,223 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the "Elastic License - * 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side - * Public License v 1"; you may not use this file except in compliance with, at - * your election, the "Elastic License 2.0", the "GNU Affero General Public - * License v3.0 only", or the "Server Side Public License, v 1". - */ - -import '../../application/models/sense_editor/sense_editor.test.mocks'; - -import $ from 'jquery'; - -// TODO: -// We import from application models as a convenient way to bootstrap loading up of an editor using -// this lib. We also need to import application specific mocks which is not ideal. -// In this situation, the token provider lib knows about app models in tests, which it really shouldn't. Should create -// a better sandbox in future. -import { create, SenseEditor } from '../../application/models/sense_editor'; - -import { Position, Token, TokensProvider } from '../../types'; - -interface RunTestArgs { - input: string; - done?: () => void; -} - -describe('Ace (legacy) token provider', () => { - let senseEditor: SenseEditor; - let tokenProvider: TokensProvider; - beforeEach(() => { - // Set up our document body - document.body.innerHTML = `
-
-
-
-
`; - - senseEditor = create(document.querySelector('#ConAppEditor')!); - - $(senseEditor.getCoreEditor().getContainer())!.show(); - - (senseEditor as any).autocomplete._test.removeChangeListener(); - tokenProvider = senseEditor.getCoreEditor().getTokenProvider(); - }); - - afterEach(async () => { - $(senseEditor.getCoreEditor().getContainer())!.hide(); - (senseEditor as any).autocomplete._test.addChangeListener(); - await senseEditor.update('', true); - }); - - describe('#getTokens', () => { - const runTest = ({ - input, - expectedTokens, - done, - lineNumber = 1, - }: RunTestArgs & { expectedTokens: Token[] | null; lineNumber?: number }) => { - senseEditor.update(input, true).then(() => { - const tokens = tokenProvider.getTokens(lineNumber); - expect(tokens).toEqual(expectedTokens); - if (done) done(); - }); - }; - - describe('base cases', () => { - test('case 1 - only url', (done) => { - runTest({ - input: `GET http://somehost/_search`, - expectedTokens: [ - { type: 'method', value: 'GET', position: { lineNumber: 1, column: 1 } }, - { type: 'whitespace', value: ' ', position: { lineNumber: 1, column: 4 } }, - { - type: 'url.protocol_host', - value: 'http://somehost', - position: { lineNumber: 1, column: 5 }, - }, - { type: 'url.slash', value: '/', position: { lineNumber: 1, column: 20 } }, - { type: 'url.part', value: '_search', position: { lineNumber: 1, column: 21 } }, - ], - done, - }); - }); - - test('case 2 - basic auth in host name', (done) => { - runTest({ - input: `GET http://test:user@somehost/`, - expectedTokens: [ - { type: 'method', value: 'GET', position: { lineNumber: 1, column: 1 } }, - { type: 'whitespace', value: ' ', position: { lineNumber: 1, column: 4 } }, - { - type: 'url.protocol_host', - value: 'http://test:user@somehost', - position: { lineNumber: 1, column: 5 }, - }, - { type: 'url.slash', value: '/', position: { lineNumber: 1, column: 30 } }, - ], - done, - }); - }); - - test('case 3 - handles empty lines', (done) => { - runTest({ - input: `POST abc - - -{ -`, - expectedTokens: [ - { type: 'method', value: 'POST', position: { lineNumber: 1, column: 1 } }, - { type: 'whitespace', value: ' ', position: { lineNumber: 1, column: 5 } }, - { type: 'url.part', value: 'abc', position: { lineNumber: 1, column: 6 } }, - ], - done, - lineNumber: 1, - }); - }); - }); - - describe('with newlines', () => { - test('case 1 - newlines base case', (done) => { - runTest({ - input: `GET http://test:user@somehost/ -{ - "wudup": "!" -}`, - expectedTokens: [ - { type: 'whitespace', value: ' ', position: { lineNumber: 3, column: 1 } }, - { type: 'variable', value: '"wudup"', position: { lineNumber: 3, column: 3 } }, - { type: 'punctuation.colon', value: ':', position: { lineNumber: 3, column: 10 } }, - { type: 'whitespace', value: ' ', position: { lineNumber: 3, column: 11 } }, - { type: 'string', value: '"!"', position: { lineNumber: 3, column: 12 } }, - ], - done, - lineNumber: 3, - }); - }); - }); - - describe('edge cases', () => { - test('case 1 - getting token outside of document', (done) => { - runTest({ - input: `GET http://test:user@somehost/ -{ - "wudup": "!" -}`, - expectedTokens: null, - done, - lineNumber: 100, - }); - }); - - test('case 2 - empty lines', (done) => { - runTest({ - input: `GET http://test:user@somehost/ - - - - -{ - "wudup": "!" -}`, - expectedTokens: [], - done, - lineNumber: 5, - }); - }); - }); - }); - - describe('#getTokenAt', () => { - const runTest = ({ - input, - expectedToken, - done, - position, - }: RunTestArgs & { expectedToken: Token | null; position: Position }) => { - senseEditor.update(input, true).then(() => { - const tokens = tokenProvider.getTokenAt(position); - expect(tokens).toEqual(expectedToken); - if (done) done(); - }); - }; - - describe('base cases', () => { - it('case 1 - gets a token from the url', (done) => { - const input = `GET http://test:user@somehost/`; - runTest({ - input, - expectedToken: { - position: { lineNumber: 1, column: 4 }, - type: 'whitespace', - value: ' ', - }, - position: { lineNumber: 1, column: 5 }, - }); - - runTest({ - input, - expectedToken: { - position: { lineNumber: 1, column: 5 }, - type: 'url.protocol_host', - value: 'http://test:user@somehost', - }, - position: { lineNumber: 1, column: input.length }, - done, - }); - }); - }); - - describe('special cases', () => { - it('case 1 - handles input outside of range', (done) => { - runTest({ - input: `GET abc`, - expectedToken: null, - done, - position: { lineNumber: 1, column: 99 }, - }); - }); - }); - }); -}); diff --git a/src/plugins/console/public/lib/ace_token_provider/token_provider.ts b/src/plugins/console/public/lib/ace_token_provider/token_provider.ts deleted file mode 100644 index 9e61771946771..0000000000000 --- a/src/plugins/console/public/lib/ace_token_provider/token_provider.ts +++ /dev/null @@ -1,84 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the "Elastic License - * 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side - * Public License v 1"; you may not use this file except in compliance with, at - * your election, the "Elastic License 2.0", the "GNU Affero General Public - * License v3.0 only", or the "Server Side Public License, v 1". - */ - -import { IEditSession, TokenInfo as BraceTokenInfo } from 'brace'; -import { TokensProvider, Token, Position } from '../../types'; - -// Brace's token information types are not accurate. -interface TokenInfo extends BraceTokenInfo { - type: string; -} - -const toToken = (lineNumber: number, column: number, token: TokenInfo): Token => ({ - type: token.type, - value: token.value, - position: { - lineNumber, - column, - }, -}); - -const toTokens = (lineNumber: number, tokens: TokenInfo[]): Token[] => { - let acc = ''; - return tokens.map((token) => { - const column = acc.length + 1; - acc += token.value; - return toToken(lineNumber, column, token); - }); -}; - -const extractTokenFromAceTokenRow = ( - lineNumber: number, - column: number, - aceTokens: TokenInfo[] -) => { - let acc = ''; - for (const token of aceTokens) { - const start = acc.length + 1; - acc += token.value; - const end = acc.length; - if (column < start) continue; - if (column > end + 1) continue; - return toToken(lineNumber, start, token); - } - return null; -}; - -export class AceTokensProvider implements TokensProvider { - constructor(private readonly session: IEditSession) {} - - getTokens(lineNumber: number): Token[] | null { - if (lineNumber < 1) return null; - - // Important: must use a .session.getLength because this is a cached value. - // Calculating line length here will lead to performance issues because this function - // may be called inside of tight loops. - const lineCount = this.session.getLength(); - if (lineNumber > lineCount) { - return null; - } - - const tokens = this.session.getTokens(lineNumber - 1) as unknown as TokenInfo[]; - if (!tokens || !tokens.length) { - // We are inside of the document but have no tokens for this line. Return an empty - // array to represent this empty line. - return []; - } - - return toTokens(lineNumber, tokens); - } - - getTokenAt(pos: Position): Token | null { - const tokens = this.session.getTokens(pos.lineNumber - 1) as unknown as TokenInfo[]; - if (tokens) { - return extractTokenFromAceTokenRow(pos.lineNumber, pos.column, tokens); - } - return null; - } -} diff --git a/src/plugins/console/public/lib/autocomplete/autocomplete.ts b/src/plugins/console/public/lib/autocomplete/autocomplete.ts deleted file mode 100644 index 73ef1981cfc0b..0000000000000 --- a/src/plugins/console/public/lib/autocomplete/autocomplete.ts +++ /dev/null @@ -1,1316 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the "Elastic License - * 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side - * Public License v 1"; you may not use this file except in compliance with, at - * your election, the "Elastic License 2.0", the "GNU Affero General Public - * License v3.0 only", or the "Server Side Public License, v 1". - */ - -import _ from 'lodash'; -import { i18n } from '@kbn/i18n'; - -// TODO: All of these imports need to be moved to the core editor so that it can inject components from there. -import { - getEndpointBodyCompleteComponents, - getGlobalAutocompleteComponents, - getTopLevelUrlCompleteComponents, - getUnmatchedEndpointComponents, -} from '../kb/kb'; - -import { createTokenIterator } from '../../application/factories'; -import type { CoreEditor, Position, Range, Token } from '../../types'; -import type RowParser from '../row_parser'; - -import * as utils from '../utils'; - -import { populateContext } from './engine'; -import type { AutoCompleteContext, DataAutoCompleteRulesOneOf, ResultTerm } from './types'; -import { URL_PATH_END_MARKER, ConstantComponent } from './components'; -import { looksLikeTypingIn } from './looks_like_typing_in'; - -let lastEvaluatedToken: Token | null = null; - -function isUrlParamsToken(token: { type: string } | null) { - switch ((token || {}).type) { - case 'url.param': - case 'url.equal': - case 'url.value': - case 'url.questionmark': - case 'url.amp': - return true; - default: - return false; - } -} - -/* Logs the provided arguments to the console if the `window.autocomplete_trace` flag is set to true. - * This function checks if the `autocomplete_trace` flag is enabled on the `window` object. This is - * only used when executing functional tests. - * If the flag is enabled, it logs each argument to the console. - * If an argument is an object, it is stringified before logging. - */ -const tracer = (...args: any[]) => { - // @ts-ignore - if (window.autocomplete_trace) { - // eslint-disable-next-line no-console - console.log.call( - console, - ..._.map(args, (arg) => { - return typeof arg === 'object' ? JSON.stringify(arg) : arg; - }) - ); - } -}; - -/** - * Get the method and token paths for a specific position in the current editor buffer. - * - * This function can be used for getting autocomplete information or for getting more information - * about the endpoint associated with autocomplete. In future, these concerns should be better - * separated. - * - */ -export function getCurrentMethodAndTokenPaths( - editor: CoreEditor, - pos: Position, - parser: RowParser, - forceEndOfUrl?: boolean /* Flag for indicating whether we want to avoid early escape optimization. */ -) { - const tokenIter = createTokenIterator({ - editor, - position: pos, - }); - const startPos = pos; - let bodyTokenPath: string[] | null = []; - const ret: AutoCompleteContext = {}; - - const STATES = { - looking_for_key: 0, // looking for a key but without jumping over anything but white space and colon. - looking_for_scope_start: 1, // skip everything until scope start - start: 3, - }; - let state = STATES.start; - - // initialization problems - - let t = tokenIter.getCurrentToken(); - if (t) { - if (startPos.column === 1) { - // if we are at the beginning of the line, the current token is the one after cursor, not before which - // deviates from the standard. - t = tokenIter.stepBackward(); - state = STATES.looking_for_scope_start; - } - } else { - if (startPos.column === 1) { - // empty lines do no have tokens, move one back - t = tokenIter.stepBackward(); - state = STATES.start; - } - } - - let walkedSomeBody = false; - - // climb one scope at a time and get the scope key - for (; t && t.type.indexOf('url') === -1 && t.type !== 'method'; t = tokenIter.stepBackward()) { - if (t.type !== 'whitespace') { - walkedSomeBody = true; - } // marks we saw something - - switch (t.type) { - case 'variable': - if (state === STATES.looking_for_key) { - bodyTokenPath.unshift(t.value.trim().replace(/"/g, '')); - } - state = STATES.looking_for_scope_start; // skip everything until the beginning of this scope - break; - - case 'paren.lparen': - bodyTokenPath.unshift(t.value); - if (state === STATES.looking_for_scope_start) { - // found it. go look for the relevant key - state = STATES.looking_for_key; - } - break; - case 'paren.rparen': - // reset he search for key - state = STATES.looking_for_scope_start; - // and ignore this sub scope.. - let parenCount = 1; - t = tokenIter.stepBackward(); - while (t && parenCount > 0) { - switch (t.type) { - case 'paren.lparen': - parenCount--; - break; - case 'paren.rparen': - parenCount++; - break; - } - if (parenCount > 0) { - t = tokenIter.stepBackward(); - } - } - if (!t) { - tracer(`paren.rparen: oops we run out.. we don't know what's up return null`); - return {}; - } - continue; - case 'punctuation.end_triple_quote': - // reset the search for key - state = STATES.looking_for_scope_start; - for (t = tokenIter.stepBackward(); t; t = tokenIter.stepBackward()) { - if (t.type === 'punctuation.start_triple_quote') { - t = tokenIter.stepBackward(); - break; - } - } - if (!t) { - tracer(`paren.rparen: oops we run out.. we don't know what's up return null`); - return {}; - } - continue; - case 'punctuation.start_triple_quote': - if (state === STATES.start) { - state = STATES.looking_for_key; - } else if (state === STATES.looking_for_key) { - state = STATES.looking_for_scope_start; - } - bodyTokenPath.unshift('"""'); - continue; - case 'string': - case 'constant.numeric': - case 'constant.language.boolean': - case 'text': - if (state === STATES.start) { - state = STATES.looking_for_key; - } else if (state === STATES.looking_for_key) { - state = STATES.looking_for_scope_start; - } - - break; - case 'punctuation.comma': - if (state === STATES.start) { - state = STATES.looking_for_scope_start; - } - break; - case 'punctuation.colon': - case 'whitespace': - if (state === STATES.start) { - state = STATES.looking_for_key; - } - break; // skip white space - } - } - - if (walkedSomeBody && (!bodyTokenPath || bodyTokenPath.length === 0) && !forceEndOfUrl) { - tracer( - 'we had some content and still no path', - '-> the cursor is position after a closed body', - '-> no auto complete' - ); - return {}; - } - - ret.urlTokenPath = []; - if (tokenIter.getCurrentPosition().lineNumber === startPos.lineNumber) { - if (t && (t.type === 'url.part' || t.type === 'url.param' || t.type === 'url.value')) { - // we are forcing the end of the url for the purposes of determining an endpoint - if (forceEndOfUrl && t.type === 'url.part') { - ret.urlTokenPath.push(t.value); - ret.urlTokenPath.push(URL_PATH_END_MARKER); - } - // we are on the same line as cursor and dealing with a url. Current token is not part of the context - t = tokenIter.stepBackward(); - // This will force method parsing - while (t!.type === 'whitespace') { - t = tokenIter.stepBackward(); - } - } - bodyTokenPath = null; // no not on a body line. - } - - ret.bodyTokenPath = bodyTokenPath; - - ret.urlParamsTokenPath = null; - ret.requestStartRow = tokenIter.getCurrentPosition().lineNumber; - let curUrlPart: - | null - | string - | Array> - | undefined - | Record; - - while (t && isUrlParamsToken(t)) { - switch (t.type) { - case 'url.value': - if (Array.isArray(curUrlPart)) { - curUrlPart.unshift(t.value); - } else if (curUrlPart) { - curUrlPart = [t.value, curUrlPart]; - } else { - curUrlPart = t.value; - } - break; - case 'url.comma': - if (!curUrlPart) { - curUrlPart = []; - } else if (!Array.isArray(curUrlPart)) { - curUrlPart = [curUrlPart]; - } - break; - case 'url.param': - const v = curUrlPart; - curUrlPart = {}; - curUrlPart[t.value] = v; - break; - case 'url.amp': - case 'url.questionmark': - if (!ret.urlParamsTokenPath) { - ret.urlParamsTokenPath = []; - } - ret.urlParamsTokenPath.unshift((curUrlPart as Record) || {}); - curUrlPart = null; - break; - } - t = tokenIter.stepBackward(); - } - - curUrlPart = null; - while (t && t.type.indexOf('url') !== -1) { - switch (t.type) { - case 'url.part': - if (Array.isArray(curUrlPart)) { - curUrlPart.unshift(t.value); - } else if (curUrlPart) { - curUrlPart = [t.value, curUrlPart]; - } else { - curUrlPart = t.value; - } - break; - case 'url.comma': - if (!curUrlPart) { - curUrlPart = []; - } else if (!Array.isArray(curUrlPart)) { - curUrlPart = [curUrlPart]; - } - break; - case 'url.slash': - if (curUrlPart) { - ret.urlTokenPath.unshift(curUrlPart as string); - curUrlPart = null; - } - break; - } - t = parser.prevNonEmptyToken(tokenIter); - } - - if (curUrlPart) { - ret.urlTokenPath.unshift(curUrlPart as string); - } - - if (!ret.bodyTokenPath && !ret.urlParamsTokenPath) { - if (ret.urlTokenPath.length > 0) { - // // started on the url, first token is current token - ret.otherTokenValues = ret.urlTokenPath[0]; - } - } else { - // mark the url as completed. - ret.urlTokenPath.push(URL_PATH_END_MARKER); - } - - if (t && t.type === 'method') { - ret.method = t.value; - } - return ret; -} - -// eslint-disable-next-line import/no-default-export -export default function ({ - coreEditor: editor, - parser, -}: { - coreEditor: CoreEditor; - parser: RowParser; -}) { - function isUrlPathToken(token: Token | null) { - switch ((token || ({} as Token)).type) { - case 'url.slash': - case 'url.comma': - case 'url.part': - return true; - default: - return false; - } - } - - function addMetaToTermsList(list: ResultTerm[], meta: string, template?: string): ResultTerm[] { - return _.map(list, function (t) { - if (typeof t !== 'object') { - t = { name: t }; - } - return _.defaults(t, { meta, template }); - }); - } - - function replaceLinesWithPrefixPieces(prefixPieces: string[], startLineNumber: number) { - const middlePiecesCount = prefixPieces.length - 1; - prefixPieces.forEach((piece, index) => { - if (index >= middlePiecesCount) { - return; - } - const line = startLineNumber + index + 1; - const column = editor.getLineValue(line).length - 1; - const start = { lineNumber: line, column: 0 }; - const end = { lineNumber: line, column }; - editor.replace({ start, end }, piece); - }); - } - - /** - * Get a different set of templates based on the value configured in the request. - * For example, when creating a snapshot repository of different types (`fs`, `url` etc), - * different properties are inserted in the textarea based on the type. - * E.g. https://github.com/elastic/kibana/blob/main/src/plugins/console/server/lib/spec_definitions/json/overrides/snapshot.create_repository.json - */ - function getConditionalTemplate( - name: string, - autocompleteRules: Record | null | undefined - ) { - const obj = autocompleteRules && autocompleteRules[name]; - - if (obj) { - const currentLineNumber = editor.getCurrentPosition().lineNumber; - - if (hasOneOfIn(obj)) { - // Get the line number of value that should provide different templates based on that - const startLine = getStartLineNumber(currentLineNumber, obj.__one_of); - // Join line values from start to current line - const lines = editor.getLines(startLine, currentLineNumber).join('\n'); - // Get the correct template by comparing the autocomplete rules against the lines - const prop = getProperty(lines, obj.__one_of); - if (prop && prop.__template) { - return prop.__template; - } - } - } - } - - /** - * Check if object has a property of '__one_of' - */ - function hasOneOfIn(value: unknown): value is { __one_of: DataAutoCompleteRulesOneOf[] } { - return typeof value === 'object' && value !== null && '__one_of' in value; - } - - /** - * Get the start line of value that matches the autocomplete rules condition - */ - function getStartLineNumber(currentLine: number, rules: DataAutoCompleteRulesOneOf[]): number { - if (currentLine === 1) { - return currentLine; - } - const value = editor.getLineValue(currentLine); - const prop = getProperty(value, rules); - if (prop) { - return currentLine; - } - return getStartLineNumber(currentLine - 1, rules); - } - - /** - * Get the matching property based on the given condition - */ - function getProperty(condition: string, rules: DataAutoCompleteRulesOneOf[]) { - return rules.find((rule) => { - if (rule.__condition && rule.__condition.lines_regex) { - return new RegExp(rule.__condition.lines_regex, 'm').test(condition); - } - return false; - }); - } - - function applyTerm(term: ResultTerm) { - const context = term.context!; - - if (context?.endpoint && term.value) { - const { data_autocomplete_rules: autocompleteRules } = context.endpoint; - const template = getConditionalTemplate(term.value, autocompleteRules); - if (template) { - term.template = template; - } - } - // make sure we get up to date replacement info. - addReplacementInfoToContext(context, editor.getCurrentPosition(), term.insertValue); - - let termAsString; - if (context.autoCompleteType === 'body') { - termAsString = - typeof term.insertValue === 'string' ? '"' + term.insertValue + '"' : term.insertValue + ''; - if (term.insertValue === '[' || term.insertValue === '{') { - termAsString = ''; - } - } else { - termAsString = term.insertValue + ''; - } - - let valueToInsert = termAsString; - let templateInserted = false; - if (context.addTemplate && !_.isUndefined(term.template) && !_.isNull(term.template)) { - let indentedTemplateLines; - // In order to allow triple quoted strings in template completion we check the `__raw_` - // attribute to determine whether this template should go through JSON formatting. - if (term.template.__raw && term.template.value) { - indentedTemplateLines = term.template.value.split('\n'); - } else { - indentedTemplateLines = utils.jsonToString(term.template, true).split('\n'); - } - let currentIndentation = editor.getLineValue(context.rangeToReplace!.start.lineNumber); - currentIndentation = currentIndentation.match(/^\s*/)![0]; - for ( - let i = 1; - i < indentedTemplateLines.length; - i++ // skip first line - ) { - indentedTemplateLines[i] = currentIndentation + indentedTemplateLines[i]; - } - - valueToInsert += ': ' + indentedTemplateLines.join('\n'); - templateInserted = true; - } else { - templateInserted = true; - if (term.value === '[') { - valueToInsert += '[]'; - } else if (term.value === '{') { - valueToInsert += '{}'; - } else { - templateInserted = false; - } - } - const linesToMoveDown = (context.prefixToAdd ?? '').match(/\n|\r/g)?.length ?? 0; - - let prefix = context.prefixToAdd ?? ''; - - // disable listening to the changes we are making. - editor.off('changeSelection', editorChangeListener); - - // if should add chars on the previous not empty line - if (linesToMoveDown) { - const [firstPart = '', ...prefixPieces] = context.prefixToAdd?.split(/\n|\r/g) ?? []; - const lastPart = _.last(prefixPieces) ?? ''; - const { start } = context.rangeToReplace!; - const end = { ...start, column: start.column + firstPart.length }; - - // adding only the content of prefix before newlines - editor.replace({ start, end }, firstPart); - - // replacing prefix pieces without the last one, which is handled separately - if (prefixPieces.length - 1 > 0) { - replaceLinesWithPrefixPieces(prefixPieces, start.lineNumber); - } - - // and the last prefix line, keeping the editor's own newlines. - prefix = lastPart; - context.rangeToReplace!.start.lineNumber = context.rangeToReplace!.end.lineNumber; - context.rangeToReplace!.start.column = 0; - } - - valueToInsert = prefix + valueToInsert + context.suffixToAdd; - - if (context.rangeToReplace!.start.column !== context.rangeToReplace!.end.column) { - editor.replace(context.rangeToReplace!, valueToInsert); - } else { - editor.insert(valueToInsert); - } - - editor.clearSelection(); // for some reason the above changes selection - - // go back to see whether we have one of ( : { & [ do not require a comma. All the rest do. - let newPos = { - lineNumber: context.rangeToReplace!.start.lineNumber, - column: - context.rangeToReplace!.start.column + - termAsString.length + - prefix.length + - (templateInserted ? 0 : context.suffixToAdd!.length), - }; - - const tokenIter = createTokenIterator({ - editor, - position: newPos, - }); - - if (context.autoCompleteType === 'body') { - // look for the next place stand, just after a comma, { - let nonEmptyToken = parser.nextNonEmptyToken(tokenIter); - switch (nonEmptyToken ? nonEmptyToken.type : 'NOTOKEN') { - case 'paren.rparen': - newPos = tokenIter.getCurrentPosition(); - break; - case 'punctuation.colon': - nonEmptyToken = parser.nextNonEmptyToken(tokenIter); - if ((nonEmptyToken || ({} as Token)).type === 'paren.lparen') { - nonEmptyToken = parser.nextNonEmptyToken(tokenIter); - newPos = tokenIter.getCurrentPosition(); - if (nonEmptyToken && nonEmptyToken.value.indexOf('"') === 0) { - newPos.column++; - } // don't stand on " - } - break; - case 'paren.lparen': - case 'punctuation.comma': - tokenIter.stepForward(); - newPos = tokenIter.getCurrentPosition(); - break; - } - editor.moveCursorToPosition(newPos); - } - - // re-enable listening to typing - editor.on('changeSelection', editorChangeListener); - } - - function getAutoCompleteContext(ctxEditor: CoreEditor, pos: Position) { - // deduces all the parameters need to position and insert the auto complete - const context: AutoCompleteContext = { - autoCompleteSet: null, // instructions for what can be here - endpoint: null, - urlPath: null, - method: null, - activeScheme: null, - editor: ctxEditor, - }; - - // context.updatedForToken = session.getTokenAt(pos.row, pos.column); - // - // if (!context.updatedForToken) - // context.updatedForToken = { value: "", start: pos.column }; // empty line - // - // context.updatedForToken.row = pos.row; // extend - - context.autoCompleteType = getAutoCompleteType(pos); - switch (context.autoCompleteType) { - case 'path': - addPathAutoCompleteSetToContext(context, pos); - break; - case 'url_params': - addUrlParamsAutoCompleteSetToContext(context, pos); - break; - case 'method': - addMethodAutoCompleteSetToContext(context); - break; - case 'body': - addBodyAutoCompleteSetToContext(context, pos); - break; - default: - return null; - } - - const isMappingsFetchingInProgress = - context.autoCompleteType === 'body' && !!context.asyncResultsState?.isLoading; - - if (!context.autoCompleteSet && !isMappingsFetchingInProgress) { - tracer('nothing to do..', context); - return null; - } - - addReplacementInfoToContext(context, pos); - - context.createdWithToken = _.clone(context.updatedForToken); - - return context; - } - - function getAutoCompleteType(pos: Position) { - // return "method", "path" or "body" to determine auto complete type. - - let rowMode = parser.getRowParseMode(); - - // eslint-disable-next-line no-bitwise - if (rowMode & parser.MODE.IN_REQUEST) { - return 'body'; - } - // eslint-disable-next-line no-bitwise - if (rowMode & parser.MODE.REQUEST_START) { - // on url path, url params or method. - const tokenIter = createTokenIterator({ - editor, - position: pos, - }); - let t = tokenIter.getCurrentToken(); - - while (t!.type === 'url.comma') { - t = tokenIter.stepBackward(); - } - switch (t!.type) { - case 'method': - return 'method'; - case 'whitespace': - t = parser.prevNonEmptyToken(tokenIter); - - switch ((t || ({} as Token)).type) { - case 'method': - // we moved one back - return 'path'; - break; - default: - if (isUrlPathToken(t)) { - return 'path'; - } - if (isUrlParamsToken(t)) { - return 'url_params'; - } - return null; - } - break; - default: - if (isUrlPathToken(t)) { - return 'path'; - } - if (isUrlParamsToken(t)) { - return 'url_params'; - } - return null; - } - } - - // after start to avoid single line url only requests - // eslint-disable-next-line no-bitwise - if (rowMode & parser.MODE.REQUEST_END) { - return 'body'; - } - - // in between request on an empty - if (editor.getLineValue(pos.lineNumber).trim() === '') { - // check if the previous line is a single line beginning of a new request - rowMode = parser.getRowParseMode(pos.lineNumber - 1); - if ( - // eslint-disable-next-line no-bitwise - rowMode & parser.MODE.REQUEST_START && - // eslint-disable-next-line no-bitwise - rowMode & parser.MODE.REQUEST_END - ) { - return 'body'; - } - // o.w suggest a method - return 'method'; - } - - return null; - } - - function addReplacementInfoToContext( - context: AutoCompleteContext, - pos: Position, - replacingTerm?: unknown - ) { - // extract the initial value, rangeToReplace & textBoxPosition - - // Scenarios for current token: - // - Nice token { "bla|" - // - Broken text token { bla| - // - No token : { | - // - Broken scenario { , bla| - // - Nice token, broken before: {, "bla" - - context.updatedForToken = _.clone( - editor.getTokenAt({ lineNumber: pos.lineNumber, column: pos.column }) - ); - if (!context.updatedForToken) { - context.updatedForToken = { - value: '', - type: '', - position: { column: pos.column, lineNumber: pos.lineNumber }, - }; - } // empty line - - let anchorToken = context.createdWithToken; - if (!anchorToken) { - anchorToken = context.updatedForToken; - } - - switch (context.updatedForToken.type) { - case 'variable': - case 'string': - case 'text': - case 'constant.numeric': - case 'constant.language.boolean': - case 'method': - case 'url.index': - case 'url.type': - case 'url.id': - case 'url.method': - case 'url.endpoint': - case 'url.part': - case 'url.param': - case 'url.value': - context.rangeToReplace = { - start: { lineNumber: pos.lineNumber, column: anchorToken.position.column }, - end: { - lineNumber: pos.lineNumber, - column: context.updatedForToken.position.column + context.updatedForToken.value.length, - }, - } as Range; - context.replacingToken = true; - break; - default: - if (replacingTerm && context.updatedForToken.value === replacingTerm) { - context.rangeToReplace = { - start: { lineNumber: pos.lineNumber, column: anchorToken.position.column }, - end: { - lineNumber: pos.lineNumber, - column: - context.updatedForToken.position.column + context.updatedForToken.value.length, - }, - } as Range; - context.replacingToken = true; - } else { - // standing on white space, quotes or another punctuation - no replacing - context.rangeToReplace = { - start: { lineNumber: pos.lineNumber, column: pos.column }, - end: { lineNumber: pos.lineNumber, column: pos.column }, - } as Range; - context.replacingToken = false; - } - break; - } - - context.textBoxPosition = { - lineNumber: context.rangeToReplace.start.lineNumber, - column: context.rangeToReplace.start.column, - }; - - switch (context.autoCompleteType) { - case 'path': - addPathPrefixSuffixToContext(context); - break; - case 'url_params': - addUrlParamsPrefixSuffixToContext(context); - break; - case 'method': - addMethodPrefixSuffixToContext(context); - break; - case 'body': - addBodyPrefixSuffixToContext(context); - break; - } - } - - function addCommaToPrefixOnAutocomplete( - nonEmptyToken: Token | null, - context: AutoCompleteContext, - charsToSkipOnSameLine: number = 1 - ) { - if (nonEmptyToken && nonEmptyToken.type.indexOf('url') < 0) { - const { position } = nonEmptyToken; - // if not on the first line - if (context.rangeToReplace && context.rangeToReplace.start?.lineNumber > 1) { - const prevTokenLineNumber = position.lineNumber; - const editorFromContext = context.editor as CoreEditor | undefined; - const line = editorFromContext?.getLineValue(prevTokenLineNumber) ?? ''; - const prevLineLength = line.length; - const linesToEnter = context.rangeToReplace.end.lineNumber - prevTokenLineNumber; - - const isTheSameLine = linesToEnter === 0; - let startColumn = prevLineLength + 1; - let spaces = context.rangeToReplace.start.column - 1; - - if (isTheSameLine) { - // prevent last char line from replacing - startColumn = position.column + charsToSkipOnSameLine; - // one char for pasted " and one for , - spaces = context.rangeToReplace.end.column - startColumn - 2; - } - - // go back to the end of the previous line - context.rangeToReplace = { - start: { lineNumber: prevTokenLineNumber, column: startColumn }, - end: { ...context.rangeToReplace.end }, - }; - - spaces = spaces >= 0 ? spaces : 0; - const spacesToEnter = isTheSameLine ? (spaces === 0 ? 1 : spaces) : spaces; - const newLineChars = `\n`.repeat(linesToEnter >= 0 ? linesToEnter : 0); - const whitespaceChars = ' '.repeat(spacesToEnter); - // add a comma at the end of the previous line, a new line and indentation - context.prefixToAdd = `,${newLineChars}${whitespaceChars}`; - } - } - } - - function addBodyPrefixSuffixToContext(context: AutoCompleteContext) { - // Figure out what happens next to the token to see whether it needs trailing commas etc. - - // Templates will be used if not destroying existing structure. - // -> token : {} or token ]/} or token , but not token : SOMETHING ELSE - - context.prefixToAdd = ''; - context.suffixToAdd = ''; - - let tokenIter = createTokenIterator({ - editor, - position: editor.getCurrentPosition()!, - }); - let nonEmptyToken = parser.nextNonEmptyToken(tokenIter); - switch (nonEmptyToken ? nonEmptyToken.type : 'NOTOKEN') { - case 'NOTOKEN': - case 'paren.lparen': - case 'paren.rparen': - case 'punctuation.comma': - context.addTemplate = true; - break; - case 'punctuation.colon': - // test if there is an empty object - if so we replace it - context.addTemplate = false; - - nonEmptyToken = parser.nextNonEmptyToken(tokenIter); - if (!(nonEmptyToken && nonEmptyToken.value === '{')) { - break; - } - nonEmptyToken = parser.nextNonEmptyToken(tokenIter); - if (!(nonEmptyToken && nonEmptyToken.value === '}')) { - break; - } - context.addTemplate = true; - // extend range to replace to include all up to token - context.rangeToReplace!.end.lineNumber = tokenIter.getCurrentTokenLineNumber() as number; - context.rangeToReplace!.end.column = - (tokenIter.getCurrentTokenColumn() as number) + nonEmptyToken.value.length; - - // move one more time to check if we need a trailing comma - nonEmptyToken = parser.nextNonEmptyToken(tokenIter); - switch (nonEmptyToken ? nonEmptyToken.type : 'NOTOKEN') { - case 'NOTOKEN': - case 'paren.rparen': - case 'punctuation.comma': - case 'punctuation.colon': - break; - default: - context.suffixToAdd = ', '; - } - - break; - default: - context.addTemplate = true; - context.suffixToAdd = ', '; - break; // for now play safe and do nothing. May be made smarter. - } - - // go back to see whether we have one of ( : { & [ do not require a comma. All the rest do. - tokenIter = createTokenIterator({ editor, position: editor.getCurrentPosition() }); - nonEmptyToken = tokenIter.getCurrentToken(); - let insertingRelativeToToken; // -1 is before token, 0 middle, +1 after token - if (context.replacingToken) { - insertingRelativeToToken = 0; - } else { - const pos = editor.getCurrentPosition(); - if (pos.column === context.updatedForToken!.position.column) { - insertingRelativeToToken = -1; - } else if ( - pos.column < - context.updatedForToken!.position.column + context.updatedForToken!.value.length - ) { - insertingRelativeToToken = 0; - } else { - insertingRelativeToToken = 1; - } - } - // we should actually look at what's happening before this token - if (parser.isEmptyToken(nonEmptyToken) || insertingRelativeToToken <= 0) { - nonEmptyToken = parser.prevNonEmptyToken(tokenIter); - } - - switch (nonEmptyToken ? nonEmptyToken.type : 'NOTOKEN') { - case 'NOTOKEN': - case 'paren.lparen': - case 'punctuation.comma': - case 'punctuation.colon': - case 'punctuation.start_triple_quote': - case 'method': - break; - case 'text': - case 'string': - case 'constant.numeric': - case 'constant.language.boolean': - case 'punctuation.end_triple_quote': - addCommaToPrefixOnAutocomplete(nonEmptyToken, context, nonEmptyToken?.value.length); - break; - default: - addCommaToPrefixOnAutocomplete(nonEmptyToken, context); - break; - } - - return context; - } - - function addUrlParamsPrefixSuffixToContext(context: AutoCompleteContext) { - context.prefixToAdd = ''; - context.suffixToAdd = ''; - } - - function addMethodPrefixSuffixToContext(context: AutoCompleteContext) { - context.prefixToAdd = ''; - context.suffixToAdd = ''; - const tokenIter = createTokenIterator({ editor, position: editor.getCurrentPosition() }); - const lineNumber = tokenIter.getCurrentPosition().lineNumber; - const t = parser.nextNonEmptyToken(tokenIter); - - if (tokenIter.getCurrentPosition().lineNumber !== lineNumber || !t) { - // we still have nothing next to the method, add a space.. - context.suffixToAdd = ' '; - } - } - - function addPathPrefixSuffixToContext(context: AutoCompleteContext) { - context.prefixToAdd = ''; - context.suffixToAdd = ''; - } - - function addMethodAutoCompleteSetToContext(context: AutoCompleteContext) { - context.autoCompleteSet = ['GET', 'PUT', 'POST', 'DELETE', 'HEAD', 'PATCH'].map((m, i) => ({ - name: m, - score: -i, - meta: i18n.translate('console.autocomplete.addMethodMetaText', { defaultMessage: 'method' }), - })); - } - - function addPathAutoCompleteSetToContext(context: AutoCompleteContext, pos: Position) { - const ret = getCurrentMethodAndTokenPaths(editor, pos, parser); - context.method = ret.method?.toUpperCase(); - context.token = ret.token; - context.otherTokenValues = ret.otherTokenValues; - context.urlTokenPath = ret.urlTokenPath; - - const components = getTopLevelUrlCompleteComponents(context.method); - let urlTokenPath = context.urlTokenPath; - let predicate: (term: ResultTerm) => boolean = () => true; - - const tokenIter = createTokenIterator({ editor, position: pos }); - const currentTokenType = tokenIter.getCurrentToken()?.type; - const previousTokenType = tokenIter.stepBackward()?.type; - if (!Array.isArray(urlTokenPath)) { - // skip checks for url.comma - } else if (previousTokenType === 'url.comma' && currentTokenType === 'url.comma') { - predicate = () => false; // two consecutive commas empty the autocomplete - } else if ( - (previousTokenType === 'url.part' && currentTokenType === 'url.comma') || - (previousTokenType === 'url.slash' && currentTokenType === 'url.comma') || - (previousTokenType === 'url.comma' && currentTokenType === 'url.part') - ) { - const lastUrlTokenPath = _.last(urlTokenPath) || []; // ['c', 'd'] from 'GET /a/b/c,d,' - const constantComponents = _.filter(components, (c) => c instanceof ConstantComponent); - const constantComponentNames = _.map(constantComponents, 'name'); - - // check if neither 'c' nor 'd' is a constant component name such as '_search' - if (_.every(lastUrlTokenPath, (token) => !_.includes(constantComponentNames, token))) { - urlTokenPath = urlTokenPath.slice(0, -1); // drop the last 'c,d,' part from the url path - predicate = (term) => term.meta === 'index'; // limit the autocomplete to indices only - } - } - - populateContext(urlTokenPath, context, editor, true, components); - context.autoCompleteSet = _.filter( - addMetaToTermsList(context.autoCompleteSet!, 'endpoint'), - predicate - ); - } - - function addUrlParamsAutoCompleteSetToContext(context: AutoCompleteContext, pos: Position) { - const ret = getCurrentMethodAndTokenPaths(editor, pos, parser); - context.method = ret.method; - context.otherTokenValues = ret.otherTokenValues; - context.urlTokenPath = ret.urlTokenPath; - if (!ret.urlTokenPath) { - // zero length tokenPath is true - - return context; - } - - populateContext( - ret.urlTokenPath, - context, - editor, - false, - getTopLevelUrlCompleteComponents(context.method) - ); - - if (!context.endpoint) { - return context; - } - - if (!ret.urlParamsTokenPath) { - // zero length tokenPath is true - return context; - } - let tokenPath: string[] = []; - const currentParam = ret.urlParamsTokenPath.pop(); - if (currentParam) { - tokenPath = Object.keys(currentParam); // single key object - context.otherTokenValues = currentParam[tokenPath[0]]; - } - - populateContext( - tokenPath, - context, - editor, - true, - context.endpoint.paramsAutocomplete.getTopLevelComponents(context.method) - ); - return context; - } - - function addBodyAutoCompleteSetToContext(context: AutoCompleteContext, pos: Position) { - const ret = getCurrentMethodAndTokenPaths(editor, pos, parser); - context.method = ret.method; - context.otherTokenValues = ret.otherTokenValues; - context.urlTokenPath = ret.urlTokenPath; - context.requestStartRow = ret.requestStartRow; - if (!ret.urlTokenPath) { - // zero length tokenPath is true - return context; - } - - populateContext( - ret.urlTokenPath, - context, - editor, - false, - getTopLevelUrlCompleteComponents(context.method) - ); - - context.bodyTokenPath = ret.bodyTokenPath; - if (!ret.bodyTokenPath) { - // zero length tokenPath is true - - return context; - } - - const t = editor.getTokenAt(pos); - if (t && t.type === 'punctuation.end_triple_quote' && pos.column !== t.position.column + 3) { - // skip to populate context as the current position is not on the edge of end_triple_quote - return context; - } - - // needed for scope linking + global term resolving - context.endpointComponentResolver = getEndpointBodyCompleteComponents; - context.globalComponentResolver = getGlobalAutocompleteComponents; - let components: unknown; - if (context.endpoint) { - components = context.endpoint.bodyAutocompleteRootComponents; - } else { - components = getUnmatchedEndpointComponents(); - } - populateContext(ret.bodyTokenPath, context, editor, true, components); - - return context; - } - - const evaluateCurrentTokenAfterAChange = _.debounce(function evaluateCurrentTokenAfterAChange( - pos: Position - ) { - let currentToken = editor.getTokenAt(pos)!; - tracer('has started evaluating current token', currentToken); - - if (!currentToken) { - lastEvaluatedToken = null; - currentToken = { position: { column: 0, lineNumber: 0 }, value: '', type: '' }; // empty row - } - - currentToken.position.lineNumber = pos.lineNumber; // extend token with row. Ace doesn't supply it by default - if (parser.isEmptyToken(currentToken)) { - // empty token. check what's coming next - const nextToken = editor.getTokenAt({ ...pos, column: pos.column + 1 })!; - if (parser.isEmptyToken(nextToken)) { - // Empty line, or we're not on the edge of current token. Save the current position as base - currentToken.position.column = pos.column; - lastEvaluatedToken = currentToken; - } else { - nextToken.position.lineNumber = pos.lineNumber; - lastEvaluatedToken = nextToken; - } - tracer('not starting autocomplete due to empty current token'); - return; - } - - if (!lastEvaluatedToken) { - lastEvaluatedToken = currentToken; - tracer('not starting autocomplete due to invalid last evaluated token'); - return; // wait for the next typing. - } - - if (!looksLikeTypingIn(lastEvaluatedToken, currentToken, editor)) { - tracer('not starting autocomplete', lastEvaluatedToken, '->', currentToken); - // not on the same place or nothing changed, cache and wait for the next time - lastEvaluatedToken = currentToken; - return; - } - - // don't automatically open the auto complete if some just hit enter (new line) or open a parentheses - switch (currentToken.type || 'UNKNOWN') { - case 'paren.lparen': - case 'paren.rparen': - case 'punctuation.colon': - case 'punctuation.comma': - case 'comment.line': - case 'comment.punctuation': - case 'comment.block': - case 'UNKNOWN': - tracer('not starting autocomplete for current token type', currentToken.type); - return; - } - - tracer('starting autocomplete', lastEvaluatedToken, '->', currentToken); - lastEvaluatedToken = currentToken; - editor.execCommand('startAutocomplete'); - }, - 100); - - function editorChangeListener() { - const position = editor.getCurrentPosition(); - tracer('editor changed', position); - if (position && !editor.isCompleterActive()) { - tracer('will start evaluating current token'); - evaluateCurrentTokenAfterAChange(position); - } - } - - /** - * Extracts terms from the autocomplete set. - * @param context - */ - function getTerms(context: AutoCompleteContext, autoCompleteSet: ResultTerm[]) { - const terms = _.map( - autoCompleteSet.filter((term) => Boolean(term) && term.name != null), - function (term) { - if (typeof term !== 'object') { - term = { - name: term, - }; - } else { - term = _.clone(term); - } - const defaults: { - value?: string; - meta: string; - score: number; - context: AutoCompleteContext; - completer?: { insertMatch: (v: unknown) => void }; - } = { - value: term.name + '', - meta: 'API', - score: 0, - context, - }; - // we only need our custom insertMatch behavior for the body - if (context.autoCompleteType === 'body') { - defaults.completer = { - insertMatch() { - return applyTerm(term); - }, - }; - } - return _.defaults(term, defaults); - } - ); - - terms.sort(function ( - t1: { score: number; name?: string | boolean }, - t2: { score: number; name?: string | boolean } - ) { - /* score sorts from high to low */ - if (t1.score > t2.score) { - return -1; - } - if (t1.score < t2.score) { - return 1; - } - /* names sort from low to high */ - if (t1.name! < t2.name!) { - return -1; - } - if (t1.name === t2.name) { - return 0; - } - return 1; - }); - - return terms; - } - - function getSuggestions(terms: ResultTerm[]) { - return _.map(terms, function (t, i) { - t.insertValue = t.insertValue || t.value; - t.value = '' + t.value; // normalize to strings - t.score = -i; - return t; - }); - } - - function getCompletions( - position: Position, - prefix: string, - callback: (e: Error | null, result: ResultTerm[] | null) => void, - annotationControls: { - setAnnotation: (text: string) => void; - removeAnnotation: () => void; - } - ) { - try { - const context = getAutoCompleteContext(editor, position); - - if (!context) { - tracer('zero suggestions due to invalid autocomplete context'); - callback(null, []); - } else { - if (!context.asyncResultsState?.isLoading) { - const terms = getTerms(context, context.autoCompleteSet!); - const suggestions = getSuggestions(terms); - tracer(suggestions?.length ?? 0, 'suggestions'); - callback(null, suggestions); - } - - if (context.asyncResultsState) { - annotationControls.setAnnotation( - i18n.translate('console.autocomplete.fieldsFetchingAnnotation', { - defaultMessage: 'Fields fetching is in progress', - }) - ); - - context.asyncResultsState.results.then((r) => { - const asyncSuggestions = getSuggestions(getTerms(context, r)); - tracer(asyncSuggestions?.length ?? 0, 'async suggestions'); - callback(null, asyncSuggestions); - annotationControls.removeAnnotation(); - }); - } - } - } catch (e) { - // eslint-disable-next-line no-console - console.error(e); - callback(e, null); - } - } - - editor.on('changeSelection', editorChangeListener); - - return { - getCompletions, - // TODO: This needs to be cleaned up - _test: { - getCompletions: ( - _editor: unknown, - _editSession: unknown, - pos: Position, - prefix: string, - callback: (e: Error | null, result: ResultTerm[] | null) => void, - annotationControls: { - setAnnotation: (text: string) => void; - removeAnnotation: () => void; - } - ) => getCompletions(pos, prefix, callback, annotationControls), - addReplacementInfoToContext, - addChangeListener: () => editor.on('changeSelection', editorChangeListener), - removeChangeListener: () => editor.off('changeSelection', editorChangeListener), - }, - }; -} diff --git a/src/plugins/console/public/lib/autocomplete/get_endpoint_from_position.ts b/src/plugins/console/public/lib/autocomplete/get_endpoint_from_position.ts deleted file mode 100644 index b65e277e41723..0000000000000 --- a/src/plugins/console/public/lib/autocomplete/get_endpoint_from_position.ts +++ /dev/null @@ -1,33 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the "Elastic License - * 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side - * Public License v 1"; you may not use this file except in compliance with, at - * your election, the "Elastic License 2.0", the "GNU Affero General Public - * License v3.0 only", or the "Server Side Public License, v 1". - */ - -import { CoreEditor, Position } from '../../types'; -import { getCurrentMethodAndTokenPaths } from './autocomplete'; -import type RowParser from '../row_parser'; - -import { getTopLevelUrlCompleteComponents } from '../kb/kb'; -import { populateContext } from './engine'; - -export function getEndpointFromPosition(editor: CoreEditor, pos: Position, parser: RowParser) { - const lineValue = editor.getLineValue(pos.lineNumber); - const context = { - ...getCurrentMethodAndTokenPaths( - editor, - { - column: lineValue.length + 1 /* Go to the very end of the line */, - lineNumber: pos.lineNumber, - }, - parser, - true - ), - }; - const components = getTopLevelUrlCompleteComponents(context.method); - populateContext(context.urlTokenPath, context, editor, true, components); - return context.endpoint; -} diff --git a/src/plugins/console/public/lib/autocomplete/looks_like_typing_in.test.ts b/src/plugins/console/public/lib/autocomplete/looks_like_typing_in.test.ts deleted file mode 100644 index 101fd96a79024..0000000000000 --- a/src/plugins/console/public/lib/autocomplete/looks_like_typing_in.test.ts +++ /dev/null @@ -1,224 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the "Elastic License - * 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side - * Public License v 1"; you may not use this file except in compliance with, at - * your election, the "Elastic License 2.0", the "GNU Affero General Public - * License v3.0 only", or the "Server Side Public License, v 1". - */ - -import '../../application/models/sense_editor/sense_editor.test.mocks'; - -import { looksLikeTypingIn } from './looks_like_typing_in'; -import { create } from '../../application/models'; -import type { SenseEditor } from '../../application/models'; -import type { CoreEditor, Position, Token, TokensProvider } from '../../types'; - -describe('looksLikeTypingIn', () => { - let editor: SenseEditor; - let coreEditor: CoreEditor; - let tokenProvider: TokensProvider; - - beforeEach(() => { - document.body.innerHTML = `
-
-
-
-
`; - editor = create(document.getElementById('ConAppEditor')!); - coreEditor = editor.getCoreEditor(); - tokenProvider = coreEditor.getTokenProvider(); - }); - - afterEach(async () => { - await editor.update('', true); - }); - - describe('general typing in', () => { - interface RunTestArgs { - preamble: string; - autocomplete?: string; - input: string; - } - - const runTest = async ({ preamble, autocomplete, input }: RunTestArgs) => { - const pos: Position = { lineNumber: 1, column: 1 }; - - await editor.update(preamble, true); - pos.column += preamble.length; - const lastEvaluatedToken = tokenProvider.getTokenAt(pos); - - if (autocomplete !== undefined) { - await editor.update(coreEditor.getValue() + autocomplete, true); - pos.column += autocomplete.length; - } - - await editor.update(coreEditor.getValue() + input, true); - pos.column += input.length; - const currentToken = tokenProvider.getTokenAt(pos); - - expect(lastEvaluatedToken).not.toBeNull(); - expect(currentToken).not.toBeNull(); - expect(looksLikeTypingIn(lastEvaluatedToken!, currentToken!, coreEditor)).toBe(true); - }; - - const cases: RunTestArgs[] = [ - { preamble: 'G', input: 'E' }, - { preamble: 'GET .kibana', input: '/' }, - { preamble: 'GET .kibana', input: ',' }, - { preamble: 'GET .kibana', input: '?' }, - { preamble: 'GET .kibana/', input: '_' }, - { preamble: 'GET .kibana/', input: '?' }, - { preamble: 'GET .kibana,', input: '.' }, - { preamble: 'GET .kibana,', input: '?' }, - { preamble: 'GET .kibana?', input: 'k' }, - { preamble: 'GET .kibana?k', input: '=' }, - { preamble: 'GET .kibana?k=', input: 'v' }, - { preamble: 'GET .kibana?k=v', input: '&' }, - { preamble: 'GET .kibana?k', input: '&' }, - { preamble: 'GET .kibana?k&', input: 'k' }, - { preamble: 'GET ', autocomplete: '.kibana', input: '/' }, - { preamble: 'GET ', autocomplete: '.kibana', input: ',' }, - { preamble: 'GET ', autocomplete: '.kibana', input: '?' }, - { preamble: 'GET .ki', autocomplete: 'bana', input: '/' }, - { preamble: 'GET .ki', autocomplete: 'bana', input: ',' }, - { preamble: 'GET .ki', autocomplete: 'bana', input: '?' }, - { preamble: 'GET _nodes/', autocomplete: 'stats', input: '/' }, - { preamble: 'GET _nodes/sta', autocomplete: 'ts', input: '/' }, - { preamble: 'GET _nodes/', autocomplete: 'jvm', input: ',' }, - { preamble: 'GET _nodes/j', autocomplete: 'vm', input: ',' }, - { preamble: 'GET _nodes/jvm,', autocomplete: 'os', input: ',' }, - { preamble: 'GET .kibana,', autocomplete: '.security', input: ',' }, - { preamble: 'GET .kibana,.sec', autocomplete: 'urity', input: ',' }, - { preamble: 'GET .kibana,', autocomplete: '.security', input: '/' }, - { preamble: 'GET .kibana,.sec', autocomplete: 'urity', input: '/' }, - { preamble: 'GET .kibana,', autocomplete: '.security', input: '?' }, - { preamble: 'GET .kibana,.sec', autocomplete: 'urity', input: '?' }, - { preamble: 'GET .kibana/', autocomplete: '_search', input: '?' }, - { preamble: 'GET .kibana/_se', autocomplete: 'arch', input: '?' }, - { preamble: 'GET .kibana/_search?', autocomplete: 'expand_wildcards', input: '=' }, - { preamble: 'GET .kibana/_search?exp', autocomplete: 'and_wildcards', input: '=' }, - { preamble: 'GET .kibana/_search?expand_wildcards=', autocomplete: 'all', input: '&' }, - { preamble: 'GET .kibana/_search?expand_wildcards=a', autocomplete: 'll', input: '&' }, - { preamble: 'GET _cat/indices?s=index&', autocomplete: 'expand_wildcards', input: '=' }, - { preamble: 'GET _cat/indices?s=index&exp', autocomplete: 'and_wildcards', input: '=' }, - { preamble: 'GET _cat/indices?v&', autocomplete: 'expand_wildcards', input: '=' }, - { preamble: 'GET _cat/indices?v&exp', autocomplete: 'and_wildcards', input: '=' }, - // autocomplete skips one iteration of token evaluation if user types in every letter - { preamble: 'GET .kibana', autocomplete: '/', input: '_' }, // token '/' may not be evaluated - { preamble: 'GET .kibana', autocomplete: ',', input: '.' }, // token ',' may not be evaluated - { preamble: 'GET .kibana', autocomplete: '?', input: 'k' }, // token '?' may not be evaluated - ]; - for (const c of cases) { - const name = - c.autocomplete === undefined - ? `'${c.preamble}' -> '${c.input}'` - : `'${c.preamble}' -> '${c.autocomplete}' (autocomplte) -> '${c.input}'`; - test(name, async () => runTest(c)); - } - }); - - describe('first typing in', () => { - test(`'' -> 'G'`, () => { - // this is based on an implementation within the evaluateCurrentTokenAfterAChange function - const lastEvaluatedToken = { position: { column: 0, lineNumber: 0 }, value: '', type: '' }; - lastEvaluatedToken.position.lineNumber = coreEditor.getCurrentPosition().lineNumber; - - const currentToken = { position: { column: 1, lineNumber: 1 }, value: 'G', type: 'method' }; - expect(looksLikeTypingIn(lastEvaluatedToken, currentToken, coreEditor)).toBe(true); - }); - }); - - const matrices = [ - ` -GET .kibana/ - - -` - .slice(1, -1) - .split('\n'), - ` - - POST test/_doc -{"message": "test"} - -GET /_cat/indices?v&s= - -DE -` - .slice(1, -1) - .split('\n'), - ` - -PUT test/_doc/1 -{"field": "value"} -` - .slice(1, -1) - .split('\n'), - ]; - - describe('navigating the editor via keyboard arrow keys', () => { - const runHorizontalZigzagWalkTest = async (matrix: string[]) => { - const width = matrix[0].length; - const height = matrix.length; - - await editor.update(matrix.join('\n'), true); - let lastEvaluatedToken = tokenProvider.getTokenAt(coreEditor.getCurrentPosition()); - let currentToken: Token | null; - - for (let i = 1; i < height * width * 2; i++) { - const pos = { - column: 1 + (i % width), - lineNumber: 1 + Math.floor(i / width), - }; - if (pos.lineNumber % 2 === 0) { - pos.column = width - pos.column + 1; - } - if (pos.lineNumber > height) { - pos.lineNumber = 2 * height - pos.lineNumber + 1; - } - - currentToken = tokenProvider.getTokenAt(pos); - expect(lastEvaluatedToken).not.toBeNull(); - expect(currentToken).not.toBeNull(); - expect(looksLikeTypingIn(lastEvaluatedToken!, currentToken!, coreEditor)).toBe(false); - lastEvaluatedToken = currentToken; - } - }; - - for (const matrix of matrices) { - test(`horizontal zigzag walk ${matrix[0].length}x${matrix.length} map`, () => - runHorizontalZigzagWalkTest(matrix)); - } - }); - - describe('clicking around the editor', () => { - const runRandomClickingTest = async (matrix: string[], attempts: number) => { - const width = matrix[0].length; - const height = matrix.length; - - await editor.update(matrix.join('\n'), true); - let lastEvaluatedToken = tokenProvider.getTokenAt(coreEditor.getCurrentPosition()); - let currentToken: Token | null; - - for (let i = 1; i < attempts; i++) { - const pos = { - column: Math.ceil(Math.random() * width), - lineNumber: Math.ceil(Math.random() * height), - }; - - currentToken = tokenProvider.getTokenAt(pos); - expect(lastEvaluatedToken).not.toBeNull(); - expect(currentToken).not.toBeNull(); - expect(looksLikeTypingIn(lastEvaluatedToken!, currentToken!, coreEditor)).toBe(false); - lastEvaluatedToken = currentToken; - } - }; - - for (const matrix of matrices) { - const attempts = 4 * matrix[0].length * matrix.length; - test(`random clicking ${matrix[0].length}x${matrix.length} map ${attempts} times`, () => - runRandomClickingTest(matrix, attempts)); - } - }); -}); diff --git a/src/plugins/console/public/lib/autocomplete/looks_like_typing_in.ts b/src/plugins/console/public/lib/autocomplete/looks_like_typing_in.ts deleted file mode 100644 index a22c985a943f6..0000000000000 --- a/src/plugins/console/public/lib/autocomplete/looks_like_typing_in.ts +++ /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", the "GNU Affero General Public License v3.0 only", and the "Server Side - * Public License v 1"; you may not use this file except in compliance with, at - * your election, the "Elastic License 2.0", the "GNU Affero General Public - * License v3.0 only", or the "Server Side Public License, v 1". - */ - -import type { CoreEditor, Position, Token } from '../../types'; - -enum Move { - ForwardOneCharacter = 1, - ForwardOneToken, // the column position may jump to the next token by autocomplete - ForwardTwoTokens, // the column position could jump two tokens due to autocomplete -} - -const knownTypingInTokenTypes = new Map>>([ - [ - Move.ForwardOneCharacter, - new Map>([ - // a pair of the last evaluated token type and a set of the current token types - ['', new Set(['method'])], - ['url.amp', new Set(['url.param'])], - ['url.comma', new Set(['url.part', 'url.questionmark'])], - ['url.equal', new Set(['url.value'])], - ['url.param', new Set(['url.amp', 'url.equal'])], - ['url.questionmark', new Set(['url.param'])], - ['url.slash', new Set(['url.part', 'url.questionmark'])], - ['url.value', new Set(['url.amp'])], - ]), - ], - [ - Move.ForwardOneToken, - new Map>([ - ['method', new Set(['url.part'])], - ['url.amp', new Set(['url.amp', 'url.equal'])], - ['url.comma', new Set(['url.comma', 'url.questionmark', 'url.slash'])], - ['url.equal', new Set(['url.amp'])], - ['url.param', new Set(['url.equal'])], - ['url.part', new Set(['url.comma', 'url.questionmark', 'url.slash'])], - ['url.questionmark', new Set(['url.equal'])], - ['url.slash', new Set(['url.comma', 'url.questionmark', 'url.slash'])], - ['url.value', new Set(['url.amp'])], - ['whitespace', new Set(['url.comma', 'url.questionmark', 'url.slash'])], - ]), - ], - [ - Move.ForwardTwoTokens, - new Map>([['url.part', new Set(['url.param', 'url.part'])]]), - ], -]); - -const getOneCharacterNextOnTheRight = (pos: Position, coreEditor: CoreEditor): string => { - const range = { - start: { column: pos.column + 1, lineNumber: pos.lineNumber }, - end: { column: pos.column + 2, lineNumber: pos.lineNumber }, - }; - return coreEditor.getValueInRange(range); -}; - -/** - * Examines a change from the last evaluated to the current token and one - * character next to the current token position on the right. Returns true if - * the change looks like typing in, false otherwise. - * - * This function is supposed to filter out situations where autocomplete is not - * preferable, such as clicking around the editor, navigating the editor via - * keyboard arrow keys, etc. - */ -export const looksLikeTypingIn = ( - lastEvaluatedToken: Token, - currentToken: Token, - coreEditor: CoreEditor -): boolean => { - // if the column position moves to the right in the same line and the current - // token length is 1, then user is possibly typing in a character. - if ( - lastEvaluatedToken.position.column < currentToken.position.column && - lastEvaluatedToken.position.lineNumber === currentToken.position.lineNumber && - currentToken.value.length === 1 && - getOneCharacterNextOnTheRight(currentToken.position, coreEditor) === '' - ) { - const moves = - lastEvaluatedToken.position.column + 1 === currentToken.position.column - ? [Move.ForwardOneCharacter] - : [Move.ForwardOneToken, Move.ForwardTwoTokens]; - for (const move of moves) { - const tokenTypesPairs = knownTypingInTokenTypes.get(move) ?? new Map>(); - const currentTokenTypes = tokenTypesPairs.get(lastEvaluatedToken.type) ?? new Set(); - if (currentTokenTypes.has(currentToken.type)) { - return true; - } - } - } - - // if the column or the line number have changed for the last token or - // user did not provided a new value, then we should not show autocomplete - // this guards against triggering autocomplete when clicking around the editor - if ( - lastEvaluatedToken.position.column !== currentToken.position.column || - lastEvaluatedToken.position.lineNumber !== currentToken.position.lineNumber || - lastEvaluatedToken.value === currentToken.value - ) { - return false; - } - - return true; -}; diff --git a/src/plugins/console/public/lib/autocomplete_entities/autocomplete_entities.test.js b/src/plugins/console/public/lib/autocomplete_entities/autocomplete_entities.test.js index 5901c95b9a074..0cffb157abb4c 100644 --- a/src/plugins/console/public/lib/autocomplete_entities/autocomplete_entities.test.js +++ b/src/plugins/console/public/lib/autocomplete_entities/autocomplete_entities.test.js @@ -7,7 +7,6 @@ * License v3.0 only", or the "Server Side Public License, v 1". */ -import '../../application/models/sense_editor/sense_editor.test.mocks'; import { setAutocompleteInfo, AutocompleteInfo } from '../../services'; import { expandAliases } from './expand_aliases'; import { httpServiceMock } from '@kbn/core-http-browser-mocks'; diff --git a/src/plugins/console/public/lib/curl_parsing/__fixtures__/curl_parsing.txt b/src/plugins/console/public/lib/curl_parsing/__fixtures__/curl_parsing.txt deleted file mode 100644 index b6dd39479550d..0000000000000 --- a/src/plugins/console/public/lib/curl_parsing/__fixtures__/curl_parsing.txt +++ /dev/null @@ -1,146 +0,0 @@ -========== -Curl 1 -------------------------------------- -curl -XPUT 'http://localhost:9200/twitter/tweet/1' -d '{ - "user" : "kimchy", - "post_date" : "2009-11-15T14:12:12", - "message" : "trying out Elastic Search" -}' -------------------------------------- -PUT /twitter/tweet/1 -{ - "user" : "kimchy", - "post_date" : "2009-11-15T14:12:12", - "message" : "trying out Elastic Search" -} -========== -Curl 2 -------------------------------------- -curl -XGET "localhost/twitter/tweet/1?version=2" -d '{ - "message" : "elasticsearch now has versioning support, double cool!" -}' -------------------------------------- -GET /twitter/tweet/1?version=2 -{ - "message" : "elasticsearch now has versioning support, double cool!" -} -=========== -Curl 3 -------------------------------------- -curl -XPOST https://localhost/twitter/tweet/1?version=2 -d '{ - "message" : "elasticsearch now has versioning support, double cool!" -}' -------------------------------------- -POST /twitter/tweet/1?version=2 -{ - "message" : "elasticsearch now has versioning support, double cool!" -} -========= -Curl 4 -------------------------------------- -curl -XPOST https://localhost/twitter -------------------------------------- -POST /twitter -========== -Curl 5 -------------------------------------- -curl -X POST https://localhost/twitter/ -------------------------------------- -POST /twitter/ -============= -Curl 6 -------------------------------------- -curl -s -XPOST localhost:9200/missing-test -d' -{ - "mappings": { - } -}' -------------------------------------- -POST /missing-test -{ - "mappings": { - } -} -========================= -Curl 7 -------------------------------------- -curl 'localhost:9200/missing-test/doc/_search?pretty' -d' -{ - "query": { - }, -}' -------------------------------------- -GET /missing-test/doc/_search?pretty -{ - "query": { - }, -} -=========================== -Curl 8 -------------------------------------- -curl localhost:9200/ -d' -{ - "query": { - } -}' -------------------------------------- -GET / -{ - "query": { - } -} -==================================== -Curl Script -------------------------------------- -#!bin/sh - -// test something -curl 'localhost:9200/missing-test/doc/_search?pretty' -d' -{ - "query": { - }, -}' - - -curl -XPOST https://localhost/twitter - -#someother comments -curl localhost:9200/ -d' -{ - "query": { - } -}' - - -------------------- -# test something -GET /missing-test/doc/_search?pretty -{ - "query": { - }, -} - -POST /twitter - -#someother comments -GET / -{ - "query": { - } -} -==================================== -Curl with some text -------------------------------------- -This is what I meant: - -curl 'localhost:9200/missing-test/doc/_search?' - -This, however, does work: -curl 'localhost:9200/missing/doc/_search?' -------------------- -### This is what I meant: - -GET /missing-test/doc/_search? - -### This, however, does work: -GET /missing/doc/_search? diff --git a/src/plugins/console/public/lib/curl_parsing/curl.js b/src/plugins/console/public/lib/curl_parsing/curl.js deleted file mode 100644 index 4dd09d1b7d59b..0000000000000 --- a/src/plugins/console/public/lib/curl_parsing/curl.js +++ /dev/null @@ -1,194 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the "Elastic License - * 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side - * Public License v 1"; you may not use this file except in compliance with, at - * your election, the "Elastic License 2.0", the "GNU Affero General Public - * License v3.0 only", or the "Server Side Public License, v 1". - */ - -function detectCURLinLine(line) { - // returns true if text matches a curl request - return line.match(/^\s*?curl\s+(-X[A-Z]+)?\s*['"]?.*?['"]?(\s*$|\s+?-d\s*?['"])/); -} - -export function detectCURL(text) { - // returns true if text matches a curl request - if (!text) return false; - for (const line of text.split('\n')) { - if (detectCURLinLine(line)) { - return true; - } - } - return false; -} - -export function parseCURL(text) { - let state = 'NONE'; - const out = []; - let body = []; - let line = ''; - const lines = text.trim().split('\n'); - let matches; - - const EmptyLine = /^\s*$/; - const Comment = /^\s*(?:#|\/{2,})(.*)\n?$/; - const ExecutionComment = /^\s*#!/; - const ClosingSingleQuote = /^([^']*)'/; - const ClosingDoubleQuote = /^((?:[^\\"]|\\.)*)"/; - const EscapedQuotes = /^((?:[^\\"']|\\.)+)/; - - const LooksLikeCurl = /^\s*curl\s+/; - const CurlVerb = /-X ?(GET|HEAD|POST|PUT|DELETE|PATCH)/; - - const HasProtocol = /[\s"']https?:\/\//; - const CurlRequestWithProto = /[\s"']https?:\/\/[^\/ ]+\/+([^\s"']+)/; - const CurlRequestWithoutProto = /[\s"'][^\/ ]+\/+([^\s"']+)/; - const CurlData = /^.+\s(--data|-d)\s*/; - const SenseLine = /^\s*(GET|HEAD|POST|PUT|DELETE|PATCH)\s+\/?(.+)/; - - if (lines.length > 0 && ExecutionComment.test(lines[0])) { - lines.shift(); - } - - function nextLine() { - if (line.length > 0) { - return true; - } - if (lines.length === 0) { - return false; - } - line = lines.shift().replace(/[\r\n]+/g, '\n') + '\n'; - return true; - } - - function unescapeLastBodyEl() { - const str = body.pop().replace(/\\([\\"'])/g, '$1'); - body.push(str); - } - - // Is the next char a single or double quote? - // If so remove it - function detectQuote() { - if (line.substr(0, 1) === "'") { - line = line.substr(1); - state = 'SINGLE_QUOTE'; - } else if (line.substr(0, 1) === '"') { - line = line.substr(1); - state = 'DOUBLE_QUOTE'; - } else { - state = 'UNQUOTED'; - } - } - - // Body is finished - append to output with final LF - function addBodyToOut() { - if (body.length > 0) { - out.push(body.join('')); - body = []; - } - state = 'LF'; - out.push('\n'); - } - - // If the pattern matches, then the state is about to change, - // so add the capture to the body and detect the next state - // Otherwise add the whole line - function consumeMatching(pattern) { - const matches = line.match(pattern); - if (matches) { - body.push(matches[1]); - line = line.substr(matches[0].length); - detectQuote(); - } else { - body.push(line); - line = ''; - } - } - - function parseCurlLine() { - let verb = 'GET'; - let request = ''; - let matches; - if ((matches = line.match(CurlVerb))) { - verb = matches[1]; - } - - // JS regexen don't support possessive quantifiers, so - // we need two distinct patterns - const pattern = HasProtocol.test(line) ? CurlRequestWithProto : CurlRequestWithoutProto; - - if ((matches = line.match(pattern))) { - request = matches[1]; - } - - out.push(verb + ' /' + request + '\n'); - - if ((matches = line.match(CurlData))) { - line = line.substr(matches[0].length); - detectQuote(); - if (EmptyLine.test(line)) { - line = ''; - } - } else { - state = 'NONE'; - line = ''; - out.push(''); - } - } - - while (nextLine()) { - if (state === 'SINGLE_QUOTE') { - consumeMatching(ClosingSingleQuote); - } else if (state === 'DOUBLE_QUOTE') { - consumeMatching(ClosingDoubleQuote); - unescapeLastBodyEl(); - } else if (state === 'UNQUOTED') { - consumeMatching(EscapedQuotes); - if (body.length) { - unescapeLastBodyEl(); - } - if (state === 'UNQUOTED') { - addBodyToOut(); - line = ''; - } - } - - // the BODY state (used to match the body of a Sense request) - // can be terminated early if it encounters - // a comment or an empty line - else if (state === 'BODY') { - if (Comment.test(line) || EmptyLine.test(line)) { - addBodyToOut(); - } else { - body.push(line); - line = ''; - } - } else if (EmptyLine.test(line)) { - if (state !== 'LF') { - out.push('\n'); - state = 'LF'; - } - line = ''; - } else if ((matches = line.match(Comment))) { - out.push('#' + matches[1] + '\n'); - state = 'NONE'; - line = ''; - } else if (LooksLikeCurl.test(line)) { - parseCurlLine(); - } else if ((matches = line.match(SenseLine))) { - out.push(matches[1] + ' /' + matches[2] + '\n'); - line = ''; - state = 'BODY'; - } - - // Nothing else matches, so output with a prefix of !!! for debugging purposes - else { - out.push('### ' + line); - line = ''; - } - } - - addBodyToOut(); - return out.join('').trim(); -} diff --git a/src/plugins/console/public/lib/curl_parsing/curl_parsing.test.js b/src/plugins/console/public/lib/curl_parsing/curl_parsing.test.js deleted file mode 100644 index 80a60cd259717..0000000000000 --- a/src/plugins/console/public/lib/curl_parsing/curl_parsing.test.js +++ /dev/null @@ -1,37 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the "Elastic License - * 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side - * Public License v 1"; you may not use this file except in compliance with, at - * your election, the "Elastic License 2.0", the "GNU Affero General Public - * License v3.0 only", or the "Server Side Public License, v 1". - */ - -import _ from 'lodash'; -import { detectCURL, parseCURL } from './curl'; -import curlTests from './__fixtures__/curl_parsing.txt'; - -describe('CURL', () => { - const notCURLS = ['sldhfsljfhs', 's;kdjfsldkfj curl -XDELETE ""', '{ "hello": 1 }']; - _.each(notCURLS, function (notCURL, i) { - test('cURL Detection - broken strings ' + i, function () { - expect(detectCURL(notCURL)).toEqual(false); - }); - }); - - curlTests.split(/^=+$/m).forEach(function (fixture) { - if (fixture.trim() === '') { - return; - } - fixture = fixture.split(/^-+$/m); - const name = fixture[0].trim(); - const curlText = fixture[1]; - const response = fixture[2].trim(); - - test('cURL Detection - ' + name, function () { - expect(detectCURL(curlText)).toBe(true); - const r = parseCURL(curlText); - expect(r).toEqual(response); - }); - }); -}); diff --git a/src/plugins/console/public/lib/kb/kb.test.js b/src/plugins/console/public/lib/kb/kb.test.js index 70ea0ef33ae86..7560789718e58 100644 --- a/src/plugins/console/public/lib/kb/kb.test.js +++ b/src/plugins/console/public/lib/kb/kb.test.js @@ -10,7 +10,6 @@ import _ from 'lodash'; import { populateContext } from '../autocomplete/engine'; -import '../../application/models/sense_editor/sense_editor.test.mocks'; import * as kb from '.'; import { AutocompleteInfo, setAutocompleteInfo } from '../../services'; diff --git a/src/plugins/console/public/lib/row_parser.test.ts b/src/plugins/console/public/lib/row_parser.test.ts deleted file mode 100644 index 869822b7bf055..0000000000000 --- a/src/plugins/console/public/lib/row_parser.test.ts +++ /dev/null @@ -1,107 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the "Elastic License - * 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side - * Public License v 1"; you may not use this file except in compliance with, at - * your election, the "Elastic License 2.0", the "GNU Affero General Public - * License v3.0 only", or the "Server Side Public License, v 1". - */ - -import '../application/models/legacy_core_editor/legacy_core_editor.test.mocks'; - -import RowParser from './row_parser'; -import { create, MODE } from '../application/models'; -import type { SenseEditor } from '../application/models'; -import type { CoreEditor } from '../types'; - -describe('RowParser', () => { - let editor: SenseEditor | null; - let parser: RowParser | null; - - beforeEach(function () { - // Set up our document body - document.body.innerHTML = `
-
-
-
-
`; - editor = create(document.getElementById('ConAppEditor')!); - parser = new RowParser(editor.getCoreEditor() as CoreEditor); - }); - - afterEach(function () { - editor?.getCoreEditor().destroy(); - editor = null; - parser = null; - }); - - describe('getRowParseMode', () => { - const forceRetokenize = false; - - it('should return MODE.BETWEEN_REQUESTS if line is empty', () => { - editor?.getCoreEditor().setValue('', forceRetokenize); - expect(parser?.getRowParseMode()).toBe(MODE.BETWEEN_REQUESTS); - }); - - it('should return MODE.BETWEEN_REQUESTS if line is a comment', () => { - editor?.getCoreEditor().setValue('// comment', forceRetokenize); - expect(parser?.getRowParseMode()).toBe(MODE.BETWEEN_REQUESTS); - }); - - it('should return MODE.REQUEST_START | MODE.REQUEST_END if line is a single line request', () => { - editor?.getCoreEditor().setValue('GET _search', forceRetokenize); - // eslint-disable-next-line no-bitwise - expect(parser?.getRowParseMode()).toBe(MODE.REQUEST_START | MODE.REQUEST_END); - }); - - it('should return MODE.IN_REQUEST if line is a request with an opening curly brace', () => { - editor?.getCoreEditor().setValue('{', forceRetokenize); - expect(parser?.getRowParseMode()).toBe(MODE.IN_REQUEST); - }); - - it('should return MODE.MULTI_DOC_CUR_DOC_END | MODE.IN_REQUEST if line is a multi doc request with an opening curly brace', () => { - editor?.getCoreEditor().setValue('GET _msearch\n{}\n{', forceRetokenize); - const lineNumber = editor?.getCoreEditor().getLineCount()! - 1; - expect(parser?.getRowParseMode(lineNumber)).toBe( - // eslint-disable-next-line no-bitwise - MODE.MULTI_DOC_CUR_DOC_END | MODE.IN_REQUEST - ); - }); - - it('should return MODE.MULTI_DOC_CUR_DOC_END | MODE.REQUEST_END if line is a multi doc request with a closing curly brace', () => { - editor?.getCoreEditor().setValue('GET _msearch\n{}\n{"foo": 1}\n', forceRetokenize); - const lineNumber = editor?.getCoreEditor().getLineCount()! - 1; - expect(parser?.getRowParseMode(lineNumber)).toBe( - // eslint-disable-next-line no-bitwise - MODE.MULTI_DOC_CUR_DOC_END | MODE.REQUEST_END - ); - }); - - it('should return MODE.REQUEST_START | MODE.REQUEST_END if line is a request with variables', () => { - editor?.getCoreEditor().setValue('GET /${exampleVariable}', forceRetokenize); - // eslint-disable-next-line no-bitwise - expect(parser?.getRowParseMode()).toBe(MODE.REQUEST_START | MODE.REQUEST_END); - }); - - it('should return MODE.REQUEST_START | MODE.REQUEST_END if a single request line ends with a closing curly brace', () => { - editor?.getCoreEditor().setValue('DELETE /_bar/_baz%{test}', forceRetokenize); - // eslint-disable-next-line no-bitwise - expect(parser?.getRowParseMode()).toBe(MODE.REQUEST_START | MODE.REQUEST_END); - }); - - it('should return correct modes for multiple bulk requests', () => { - editor - ?.getCoreEditor() - .setValue('POST _bulk\n{"index": {"_index": "test"}}\n{"foo": "bar"}\n', forceRetokenize); - expect(parser?.getRowParseMode(0)).toBe(MODE.BETWEEN_REQUESTS); - editor - ?.getCoreEditor() - .setValue('POST _bulk\n{"index": {"_index": "test"}}\n{"foo": "bar"}\n', forceRetokenize); - const lineNumber = editor?.getCoreEditor().getLineCount()! - 1; - expect(parser?.getRowParseMode(lineNumber)).toBe( - // eslint-disable-next-line no-bitwise - MODE.REQUEST_END | MODE.MULTI_DOC_CUR_DOC_END - ); - }); - }); -}); diff --git a/src/plugins/console/public/lib/row_parser.ts b/src/plugins/console/public/lib/row_parser.ts deleted file mode 100644 index 7078bb857d95b..0000000000000 --- a/src/plugins/console/public/lib/row_parser.ts +++ /dev/null @@ -1,161 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the "Elastic License - * 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side - * Public License v 1"; you may not use this file except in compliance with, at - * your election, the "Elastic License 2.0", the "GNU Affero General Public - * License v3.0 only", or the "Server Side Public License, v 1". - */ - -import { CoreEditor, Token } from '../types'; -import { TokenIterator } from './token_iterator'; - -export const MODE = { - REQUEST_START: 2, - IN_REQUEST: 4, - MULTI_DOC_CUR_DOC_END: 8, - REQUEST_END: 16, - BETWEEN_REQUESTS: 32, -}; - -// eslint-disable-next-line import/no-default-export -export default class RowParser { - constructor(private readonly editor: CoreEditor) {} - - MODE = MODE; - - getRowParseMode(lineNumber = this.editor.getCurrentPosition().lineNumber) { - const linesCount = this.editor.getLineCount(); - if (lineNumber > linesCount || lineNumber < 1) { - return MODE.BETWEEN_REQUESTS; - } - const mode = this.editor.getLineState(lineNumber); - - if (!mode) { - return MODE.BETWEEN_REQUESTS; - } // shouldn't really happen - // If another "start" mode is added here because we want to allow for new language highlighting - // please see https://github.com/elastic/kibana/pull/51446 for a discussion on why - // should consider a different approach. - if (mode !== 'start' && mode !== 'start-sql') { - return MODE.IN_REQUEST; - } - let line = (this.editor.getLineValue(lineNumber) || '').trim(); - - if (!line || line.startsWith('#') || line.startsWith('//') || line.startsWith('/*')) { - return MODE.BETWEEN_REQUESTS; - } // empty line or a comment waiting for a new req to start - - // Check for multi doc requests - if (line.endsWith('}') && !this.isRequestLine(line)) { - // check for a multi doc request must start a new json doc immediately after this one end. - lineNumber++; - if (lineNumber < linesCount + 1) { - line = (this.editor.getLineValue(lineNumber) || '').trim(); - if (line.indexOf('{') === 0) { - // next line is another doc in a multi doc - // eslint-disable-next-line no-bitwise - return MODE.MULTI_DOC_CUR_DOC_END | MODE.IN_REQUEST; - } - } - // eslint-disable-next-line no-bitwise - return MODE.REQUEST_END | MODE.MULTI_DOC_CUR_DOC_END; // end of request - } - - // check for single line requests - lineNumber++; - if (lineNumber >= linesCount + 1) { - // eslint-disable-next-line no-bitwise - return MODE.REQUEST_START | MODE.REQUEST_END; - } - line = (this.editor.getLineValue(lineNumber) || '').trim(); - if (line.indexOf('{') !== 0) { - // next line is another request - // eslint-disable-next-line no-bitwise - return MODE.REQUEST_START | MODE.REQUEST_END; - } - - return MODE.REQUEST_START; - } - - rowPredicate(lineNumber: number | undefined, editor: CoreEditor, value: number) { - const mode = this.getRowParseMode(lineNumber); - // eslint-disable-next-line no-bitwise - return (mode & value) > 0; - } - - isEndRequestRow(row?: number, _e?: CoreEditor) { - const editor = _e || this.editor; - return this.rowPredicate(row, editor, MODE.REQUEST_END); - } - - isRequestEdge(row?: number, _e?: CoreEditor) { - const editor = _e || this.editor; - // eslint-disable-next-line no-bitwise - return this.rowPredicate(row, editor, MODE.REQUEST_END | MODE.REQUEST_START); - } - - isStartRequestRow(row?: number, _e?: CoreEditor) { - const editor = _e || this.editor; - return this.rowPredicate(row, editor, MODE.REQUEST_START); - } - - isInBetweenRequestsRow(row?: number, _e?: CoreEditor) { - const editor = _e || this.editor; - return this.rowPredicate(row, editor, MODE.BETWEEN_REQUESTS); - } - - isInRequestsRow(row?: number, _e?: CoreEditor) { - const editor = _e || this.editor; - return this.rowPredicate(row, editor, MODE.IN_REQUEST); - } - - isMultiDocDocEndRow(row?: number, _e?: CoreEditor) { - const editor = _e || this.editor; - return this.rowPredicate(row, editor, MODE.MULTI_DOC_CUR_DOC_END); - } - - isEmptyToken(tokenOrTokenIter: TokenIterator | Token | null) { - const token = - tokenOrTokenIter && (tokenOrTokenIter as TokenIterator).getCurrentToken - ? (tokenOrTokenIter as TokenIterator).getCurrentToken() - : tokenOrTokenIter; - return !token || (token as Token).type === 'whitespace'; - } - - isUrlOrMethodToken(tokenOrTokenIter: TokenIterator | Token) { - const t = (tokenOrTokenIter as TokenIterator)?.getCurrentToken() ?? (tokenOrTokenIter as Token); - return t && t.type && (t.type === 'method' || t.type.indexOf('url') === 0); - } - - nextNonEmptyToken(tokenIter: TokenIterator) { - let t = tokenIter.stepForward(); - while (t && this.isEmptyToken(t)) { - t = tokenIter.stepForward(); - } - return t; - } - - prevNonEmptyToken(tokenIter: TokenIterator) { - let t = tokenIter.stepBackward(); - // empty rows return null token. - while ((t || tokenIter.getCurrentPosition().lineNumber > 1) && this.isEmptyToken(t)) - t = tokenIter.stepBackward(); - return t; - } - - isCommentToken(token: Token | null) { - return ( - token && - token.type && - (token.type === 'comment.punctuation' || - token.type === 'comment.line' || - token.type === 'comment.block') - ); - } - - isRequestLine(line: string) { - const methods = ['GET', 'POST', 'PUT', 'DELETE', 'HEAD', 'PATCH', 'OPTIONS']; - return methods.some((m) => line.startsWith(m)); - } -} diff --git a/src/plugins/console/public/styles/_app.scss b/src/plugins/console/public/styles/_app.scss index 4c3ccb8b1cadc..0f0a671d920c3 100644 --- a/src/plugins/console/public/styles/_app.scss +++ b/src/plugins/console/public/styles/_app.scss @@ -36,8 +36,6 @@ width: 100%; display: flex; flex: 0 0 auto; - - // Required on IE11 to render ace editor correctly after first input. position: relative; &__spinner { @@ -55,46 +53,6 @@ height: 100%; display: flex; flex: 1 1 1px; - - .ace_badge { - font-family: $euiFontFamily; - font-size: $euiFontSizeXS; - font-weight: $euiFontWeightMedium; - line-height: $euiLineHeight; - padding: 0 $euiSizeS; - display: inline-block; - text-decoration: none; - border-radius: calc($euiBorderRadius / 2); - white-space: nowrap; - vertical-align: middle; - cursor: default; - max-width: 100%; - - &--success { - background-color: $euiColorVis0_behindText; - color: chooseLightOrDarkText($euiColorVis0_behindText); - } - - &--warning { - background-color: $euiColorVis5_behindText; - color: chooseLightOrDarkText($euiColorVis5_behindText); - } - - &--primary { - background-color: $euiColorVis1_behindText; - color: chooseLightOrDarkText($euiColorVis1_behindText); - } - - &--default { - background-color: $euiColorLightShade; - color: chooseLightOrDarkText($euiColorLightShade); - } - - &--danger { - background-color: $euiColorVis9_behindText; - color: chooseLightOrDarkText($euiColorVis9_behindText); - } - } } .conApp__editorContent, @@ -145,17 +103,6 @@ margin-inline: 0; } -// SASSTODO: This component seems to not be used anymore? -// Possibly replaced by the Ace version -.conApp__autoComplete { - position: absolute; - left: -1000px; - visibility: hidden; - /* by pass any other element in ace and resize bar, but not modal popups */ - z-index: $euiZLevel1 + 2; - margin-top: 22px; -} - .conApp__requestProgressBarContainer { position: relative; z-index: $euiZLevel2; diff --git a/src/plugins/console/public/types/core_editor.ts b/src/plugins/console/public/types/core_editor.ts index aa9bdf21c1c94..8d5ab2a582226 100644 --- a/src/plugins/console/public/types/core_editor.ts +++ b/src/plugins/console/public/types/core_editor.ts @@ -7,7 +7,6 @@ * License v3.0 only", or the "Server Side Public License, v 1". */ -import type { Editor } from 'brace'; import { ResultTerm } from '../lib/autocomplete/types'; import { TokensProvider } from './tokens_provider'; import { Token } from './token'; @@ -94,7 +93,7 @@ export enum LINE_MODE { /** * The CoreEditor is a component separate from the Editor implementation that provides Console * app specific business logic. The CoreEditor is an interface to the lower-level editor implementation - * being used which is usually vendor code such as Ace or Monaco. + * being used which is usually vendor code such as Monaco. */ export interface CoreEditor { /** @@ -260,7 +259,7 @@ export interface CoreEditor { */ registerKeyboardShortcut(opts: { keys: string | { win?: string; mac?: string }; - fn: (editor: Editor) => void; + fn: (editor: any) => void; name: string; }): void; diff --git a/src/plugins/console/tsconfig.json b/src/plugins/console/tsconfig.json index 2b0f6127cd4af..02e4e7a9b7689 100644 --- a/src/plugins/console/tsconfig.json +++ b/src/plugins/console/tsconfig.json @@ -18,10 +18,8 @@ "@kbn/i18n-react", "@kbn/shared-ux-utility", "@kbn/core-http-browser", - "@kbn/ace", "@kbn/config-schema", "@kbn/core-http-router-server-internal", - "@kbn/web-worker-stub", "@kbn/core-elasticsearch-server", "@kbn/core-http-browser-mocks", "@kbn/react-kibana-context-theme", diff --git a/src/plugins/dashboard/common/dashboard_saved_object/persistable_state/dashboard_saved_object_references.ts b/src/plugins/dashboard/common/dashboard_saved_object/persistable_state/dashboard_saved_object_references.ts index 80644fa94dc36..1ede56a2b67a7 100644 --- a/src/plugins/dashboard/common/dashboard_saved_object/persistable_state/dashboard_saved_object_references.ts +++ b/src/plugins/dashboard/common/dashboard_saved_object/persistable_state/dashboard_saved_object_references.ts @@ -16,6 +16,10 @@ import { } from '../../lib/dashboard_panel_converters'; import { DashboardAttributesAndReferences, ParsedDashboardAttributesWithType } from '../../types'; import { DashboardAttributes, SavedDashboardPanel } from '../../content_management'; +import { + createExtract, + createInject, +} from '../../dashboard_container/persistable_state/dashboard_container_references'; export interface InjectExtractDeps { embeddablePersistableStateService: EmbeddablePersistableStateService; @@ -45,10 +49,8 @@ export function injectReferences( const parsedAttributes = parseDashboardAttributesWithType(attributes); // inject references back into panels via the Embeddable persistable state service. - const injectedState = deps.embeddablePersistableStateService.inject( - parsedAttributes, - references - ) as ParsedDashboardAttributesWithType; + const inject = createInject(deps.embeddablePersistableStateService); + const injectedState = inject(parsedAttributes, references) as ParsedDashboardAttributesWithType; const injectedPanels = convertPanelMapToSavedPanels(injectedState.panels); const newAttributes = { @@ -74,11 +76,11 @@ export function extractReferences( ); } - const { references: extractedReferences, state: extractedState } = - deps.embeddablePersistableStateService.extract(parsedAttributes) as { - references: Reference[]; - state: ParsedDashboardAttributesWithType; - }; + const extract = createExtract(deps.embeddablePersistableStateService); + const { references: extractedReferences, state: extractedState } = extract(parsedAttributes) as { + references: Reference[]; + state: ParsedDashboardAttributesWithType; + }; const extractedPanels = convertPanelMapToSavedPanels(extractedState.panels); const newAttributes = { diff --git a/src/plugins/dashboard/public/dashboard_container/external_api/dashboard_renderer.test.tsx b/src/plugins/dashboard/public/dashboard_container/external_api/dashboard_renderer.test.tsx index fd41fdd5e764d..6a81a8c4fd601 100644 --- a/src/plugins/dashboard/public/dashboard_container/external_api/dashboard_renderer.test.tsx +++ b/src/plugins/dashboard/public/dashboard_container/external_api/dashboard_renderer.test.tsx @@ -18,11 +18,12 @@ import { SavedObjectNotFound } from '@kbn/kibana-utils-plugin/common'; import { setStubKibanaServices as setPresentationPanelMocks } from '@kbn/presentation-panel-plugin/public/mocks'; import { BehaviorSubject } from 'rxjs'; import { DashboardContainerFactory } from '..'; -import { DASHBOARD_CONTAINER_TYPE, DashboardCreationOptions } from '../..'; -import { embeddableService } from '../../services/kibana_services'; +import { DashboardCreationOptions } from '../..'; import { DashboardContainer } from '../embeddable/dashboard_container'; import { DashboardRenderer } from './dashboard_renderer'; +jest.mock('../embeddable/dashboard_container_factory', () => ({})); + describe('dashboard renderer', () => { let mockDashboardContainer: DashboardContainer; let mockDashboardFactory: DashboardContainerFactory; @@ -38,7 +39,10 @@ describe('dashboard renderer', () => { mockDashboardFactory = { create: jest.fn().mockReturnValue(mockDashboardContainer), } as unknown as DashboardContainerFactory; - embeddableService.getEmbeddableFactory = jest.fn().mockReturnValue(mockDashboardFactory); + // eslint-disable-next-line @typescript-eslint/no-var-requires + require('../embeddable/dashboard_container_factory').DashboardContainerFactoryDefinition = jest + .fn() + .mockReturnValue(mockDashboardFactory); setPresentationPanelMocks(); }); @@ -46,7 +50,6 @@ describe('dashboard renderer', () => { await act(async () => { mountWithIntl(); }); - expect(embeddableService.getEmbeddableFactory).toHaveBeenCalledWith(DASHBOARD_CONTAINER_TYPE); expect(mockDashboardFactory.create).toHaveBeenCalled(); }); @@ -103,7 +106,10 @@ describe('dashboard renderer', () => { mockDashboardFactory = { create: jest.fn().mockReturnValue(mockErrorEmbeddable), } as unknown as DashboardContainerFactory; - embeddableService.getEmbeddableFactory = jest.fn().mockReturnValue(mockDashboardFactory); + // eslint-disable-next-line @typescript-eslint/no-var-requires + require('../embeddable/dashboard_container_factory').DashboardContainerFactoryDefinition = jest + .fn() + .mockReturnValue(mockDashboardFactory); let wrapper: ReactWrapper; await act(async () => { @@ -125,7 +131,10 @@ describe('dashboard renderer', () => { const mockErrorFactory = { create: jest.fn().mockReturnValue(mockErrorEmbeddable), } as unknown as DashboardContainerFactory; - embeddableService.getEmbeddableFactory = jest.fn().mockReturnValue(mockErrorFactory); + // eslint-disable-next-line @typescript-eslint/no-var-requires + require('../embeddable/dashboard_container_factory').DashboardContainerFactoryDefinition = jest + .fn() + .mockReturnValue(mockErrorFactory); // render the dashboard - it should run into an error and render the error embeddable. let wrapper: ReactWrapper; @@ -146,7 +155,10 @@ describe('dashboard renderer', () => { const mockSuccessFactory = { create: jest.fn().mockReturnValue(mockSuccessEmbeddable), } as unknown as DashboardContainerFactory; - embeddableService.getEmbeddableFactory = jest.fn().mockReturnValue(mockSuccessFactory); + // eslint-disable-next-line @typescript-eslint/no-var-requires + require('../embeddable/dashboard_container_factory').DashboardContainerFactoryDefinition = jest + .fn() + .mockReturnValue(mockSuccessFactory); // update the saved object id to trigger another dashboard load. await act(async () => { @@ -175,7 +187,10 @@ describe('dashboard renderer', () => { const mockErrorFactory = { create: jest.fn().mockReturnValue(mockErrorEmbeddable), } as unknown as DashboardContainerFactory; - embeddableService.getEmbeddableFactory = jest.fn().mockReturnValue(mockErrorFactory); + // eslint-disable-next-line @typescript-eslint/no-var-requires + require('../embeddable/dashboard_container_factory').DashboardContainerFactoryDefinition = jest + .fn() + .mockReturnValue(mockErrorFactory); // render the dashboard - it should run into an error and render the error embeddable. let wrapper: ReactWrapper; @@ -238,7 +253,10 @@ describe('dashboard renderer', () => { const mockSuccessFactory = { create: jest.fn().mockReturnValue(mockSuccessEmbeddable), } as unknown as DashboardContainerFactory; - embeddableService.getEmbeddableFactory = jest.fn().mockReturnValue(mockSuccessFactory); + // eslint-disable-next-line @typescript-eslint/no-var-requires + require('../embeddable/dashboard_container_factory').DashboardContainerFactoryDefinition = jest + .fn() + .mockReturnValue(mockSuccessFactory); let wrapper: ReactWrapper; await act(async () => { @@ -263,7 +281,10 @@ describe('dashboard renderer', () => { const mockUseMarginFalseFactory = { create: jest.fn().mockReturnValue(mockUseMarginFalseEmbeddable), } as unknown as DashboardContainerFactory; - embeddableService.getEmbeddableFactory = jest.fn().mockReturnValue(mockUseMarginFalseFactory); + // eslint-disable-next-line @typescript-eslint/no-var-requires + require('../embeddable/dashboard_container_factory').DashboardContainerFactoryDefinition = jest + .fn() + .mockReturnValue(mockUseMarginFalseFactory); let wrapper: ReactWrapper; await act(async () => { diff --git a/src/plugins/dashboard/public/dashboard_container/external_api/dashboard_renderer.tsx b/src/plugins/dashboard/public/dashboard_container/external_api/dashboard_renderer.tsx index a43bd6ddbc75b..40b54e42e6ffa 100644 --- a/src/plugins/dashboard/public/dashboard_container/external_api/dashboard_renderer.tsx +++ b/src/plugins/dashboard/public/dashboard_container/external_api/dashboard_renderer.tsx @@ -20,15 +20,11 @@ import { SavedObjectNotFound } from '@kbn/kibana-utils-plugin/common'; import { useStateFromPublishingSubject } from '@kbn/presentation-publishing'; import { LocatorPublic } from '@kbn/share-plugin/common'; -import { DASHBOARD_CONTAINER_TYPE } from '..'; import { DashboardContainerInput } from '../../../common'; import { DashboardApi } from '../../dashboard_api/types'; import { embeddableService, screenshotModeService } from '../../services/kibana_services'; import type { DashboardContainer } from '../embeddable/dashboard_container'; -import { - DashboardContainerFactory, - DashboardContainerFactoryDefinition, -} from '../embeddable/dashboard_container_factory'; +import { DashboardContainerFactoryDefinition } from '../embeddable/dashboard_container_factory'; import type { DashboardCreationOptions } from '../..'; import { DashboardLocatorParams, DashboardRedirect } from '../types'; import { Dashboard404Page } from './dashboard_404'; @@ -91,12 +87,8 @@ export function DashboardRenderer({ (async () => { const creationOptions = await getCreationOptions?.(); - const dashboardFactory = embeddableService.getEmbeddableFactory( - DASHBOARD_CONTAINER_TYPE - ) as DashboardContainerFactory & { - create: DashboardContainerFactoryDefinition['create']; - }; - const container = await dashboardFactory?.create( + const dashboardFactory = new DashboardContainerFactoryDefinition(embeddableService); + const container = await dashboardFactory.create( { id } as unknown as DashboardContainerInput, // Input from creationOptions is used instead. undefined, creationOptions, diff --git a/src/plugins/dashboard/public/dashboard_container/index.ts b/src/plugins/dashboard/public/dashboard_container/index.ts index 16314f52d38f8..b4ecb30f3c25d 100644 --- a/src/plugins/dashboard/public/dashboard_container/index.ts +++ b/src/plugins/dashboard/public/dashboard_container/index.ts @@ -15,10 +15,7 @@ export const DASHBOARD_CONTAINER_TYPE = 'dashboard'; export const LATEST_DASHBOARD_CONTAINER_VERSION = convertNumberToDashboardVersion(LATEST_VERSION); export type { DashboardContainer } from './embeddable/dashboard_container'; -export { - type DashboardContainerFactory, - DashboardContainerFactoryDefinition, -} from './embeddable/dashboard_container_factory'; +export { type DashboardContainerFactory } from './embeddable/dashboard_container_factory'; export { LazyDashboardRenderer } from './external_api/lazy_dashboard_renderer'; export type { DashboardLocatorParams } from './types'; diff --git a/src/plugins/dashboard/public/plugin.tsx b/src/plugins/dashboard/public/plugin.tsx index b1d60adc84d0f..b7a920eb08ce3 100644 --- a/src/plugins/dashboard/public/plugin.tsx +++ b/src/plugins/dashboard/public/plugin.tsx @@ -72,13 +72,13 @@ import { LEGACY_DASHBOARD_APP_ID, SEARCH_SESSION_ID, } from './dashboard_constants'; -import { DashboardContainerFactoryDefinition } from './dashboard_container/embeddable/dashboard_container_factory'; import { GetPanelPlacementSettings, registerDashboardPanelPlacementSetting, } from './dashboard_container/panel_placement'; import type { FindDashboardsService } from './services/dashboard_content_management_service/types'; import { setKibanaServices, untilPluginStartServicesReady } from './services/kibana_services'; +import { buildAllDashboardActions } from './dashboard_actions'; export interface DashboardFeatureFlagConfig { allowByValueEmbeddables: boolean; @@ -227,14 +227,6 @@ export class DashboardPlugin }, }); - core.getStartServices().then(([, deps]) => { - const dashboardContainerFactory = new DashboardContainerFactoryDefinition(deps.embeddable); - embeddable.registerEmbeddableFactory( - dashboardContainerFactory.type, - dashboardContainerFactory - ); - }); - this.stopUrlTracking = () => { stopUrlTracker(); }; @@ -331,14 +323,12 @@ export class DashboardPlugin public start(core: CoreStart, plugins: DashboardStartDependencies): DashboardStart { setKibanaServices(core, plugins); - Promise.all([import('./dashboard_actions'), untilPluginStartServicesReady()]).then( - ([{ buildAllDashboardActions }]) => { - buildAllDashboardActions({ - plugins, - allowByValueEmbeddables: this.dashboardFeatureFlagConfig?.allowByValueEmbeddables, - }); - } - ); + untilPluginStartServicesReady().then(() => { + buildAllDashboardActions({ + plugins, + allowByValueEmbeddables: this.dashboardFeatureFlagConfig?.allowByValueEmbeddables, + }); + }); return { locator: this.locator, diff --git a/src/plugins/data/common/search/expressions/esql.ts b/src/plugins/data/common/search/expressions/esql.ts index b6cb039683c9b..966500710fd45 100644 --- a/src/plugins/data/common/search/expressions/esql.ts +++ b/src/plugins/data/common/search/expressions/esql.ts @@ -289,7 +289,7 @@ export const getEsqlFn = ({ getStartDependencies }: EsqlFnArguments) => { }), }) .json(params) - .ok({ json: rawResponse, requestParams }); + .ok({ json: { rawResponse }, requestParams }); }, error(error) { logInspectorRequest() diff --git a/src/plugins/data_view_management/kibana.jsonc b/src/plugins/data_view_management/kibana.jsonc index 479e357804140..5b827868ee1e8 100644 --- a/src/plugins/data_view_management/kibana.jsonc +++ b/src/plugins/data_view_management/kibana.jsonc @@ -20,6 +20,7 @@ ], "optionalPlugins": [ "noDataPage", + "share", "spaces" ], "requiredBundles": [ diff --git a/src/plugins/data_view_management/public/components/index_pattern_table/index_pattern_table.tsx b/src/plugins/data_view_management/public/components/index_pattern_table/index_pattern_table.tsx index cb93e01d1cc15..4512cb520c574 100644 --- a/src/plugins/data_view_management/public/components/index_pattern_table/index_pattern_table.tsx +++ b/src/plugins/data_view_management/public/components/index_pattern_table/index_pattern_table.tsx @@ -26,7 +26,7 @@ import { RouteComponentProps, useLocation, withRouter } from 'react-router-dom'; import useObservable from 'react-use/lib/useObservable'; import { reactRouterNavigate, useKibana } from '@kbn/kibana-react-plugin/public'; -import { NoDataViewsPromptComponent } from '@kbn/shared-ux-prompt-no-data-views'; +import { NoDataViewsPromptComponent, useOnTryESQL } from '@kbn/shared-ux-prompt-no-data-views'; import type { SpacesContextProps } from '@kbn/spaces-plugin/public'; import { DataViewType } from '@kbn/data-views-plugin/public'; import { RollupDeprecationTooltip } from '@kbn/rollup'; @@ -86,6 +86,7 @@ export const IndexPatternTable = ({ application, chrome, dataViews, + share, IndexPatternEditor, spaces, overlays, @@ -116,6 +117,12 @@ export const IndexPatternTable = ({ const hasDataView = useObservable(dataViewController.hasDataView$, defaults.hasDataView); const hasESData = useObservable(dataViewController.hasESData$, defaults.hasEsData); + const useOnTryESQLParams = { + locatorClient: share?.url.locators, + navigateToApp: application.navigateToApp, + }; + const onTryESQL = useOnTryESQL(useOnTryESQLParams); + const handleOnChange = ({ queryText, error }: { queryText: string; error: unknown }) => { if (!error) { setQuery(queryText); @@ -370,6 +377,8 @@ export const IndexPatternTable = ({ onClickCreate={() => setShowCreateDialog(true)} canCreateNewDataView={application.capabilities.indexPatterns.save as boolean} dataViewsDocLink={docLinks.links.indexPatterns.introduction} + onTryESQL={onTryESQL} + esqlDocLink={docLinks.links.query.queryESQL} emptyPromptColor={'subdued'} /> diff --git a/src/plugins/data_view_management/public/management_app/mount_management_section.tsx b/src/plugins/data_view_management/public/management_app/mount_management_section.tsx index 995d5ed977ed3..96e5ae6c96b0c 100644 --- a/src/plugins/data_view_management/public/management_app/mount_management_section.tsx +++ b/src/plugins/data_view_management/public/management_app/mount_management_section.tsx @@ -17,6 +17,7 @@ import { StartServicesAccessor } from '@kbn/core/public'; import { KibanaRenderContextProvider } from '@kbn/react-kibana-context-render'; import { KibanaContextProvider } from '@kbn/kibana-react-plugin/public'; import { ManagementAppMountParams } from '@kbn/management-plugin/public'; +import { NoDataViewsPromptKibanaProvider } from '@kbn/shared-ux-prompt-no-data-views'; import { IndexPatternTableWithRouter, EditIndexPatternContainer, @@ -64,11 +65,13 @@ export async function mountManagementSection( dataViews, fieldFormats, unifiedSearch, + share, spaces, savedObjectsManagement, }, indexPatternManagementStart, ] = await getStartServices(); + const canSave = dataViews.getCanSaveSync(); if (!canSave) { @@ -89,6 +92,7 @@ export async function mountManagementSection( chrome, uiSettings, settings, + share, notifications, overlays, unifiedSearch, @@ -115,23 +119,29 @@ export async function mountManagementSection( ReactDOM.render( - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + , params.element diff --git a/src/plugins/data_view_management/public/plugin.ts b/src/plugins/data_view_management/public/plugin.ts index 77e8c12a13ad0..0d03dc8896fd1 100644 --- a/src/plugins/data_view_management/public/plugin.ts +++ b/src/plugins/data_view_management/public/plugin.ts @@ -21,6 +21,7 @@ import { ManagementSetup } from '@kbn/management-plugin/public'; import { SavedObjectsManagementPluginStart } from '@kbn/saved-objects-management-plugin/public'; import { SpacesPluginStart } from '@kbn/spaces-plugin/public'; import { UnifiedSearchPublicPluginStart } from '@kbn/unified-search-plugin/public'; +import { SharePluginStart } from '@kbn/share-plugin/public'; export interface IndexPatternManagementSetupDependencies { management: ManagementSetup; @@ -34,6 +35,7 @@ export interface IndexPatternManagementStartDependencies { dataViewEditor: DataViewEditorStart; dataViews: DataViewsPublicPluginStart; fieldFormats: FieldFormatsStart; + share?: SharePluginStart; spaces?: SpacesPluginStart; unifiedSearch: UnifiedSearchPublicPluginStart; savedObjectsManagement: SavedObjectsManagementPluginStart; diff --git a/src/plugins/data_view_management/public/types.ts b/src/plugins/data_view_management/public/types.ts index b7a9279de8001..161ee3b1e21de 100644 --- a/src/plugins/data_view_management/public/types.ts +++ b/src/plugins/data_view_management/public/types.ts @@ -29,6 +29,7 @@ import type { SpacesPluginStart } from '@kbn/spaces-plugin/public'; import type { SavedObjectsManagementPluginStart } from '@kbn/saved-objects-management-plugin/public'; import type { SettingsStart } from '@kbn/core-ui-settings-browser'; import type { NoDataPagePluginSetup } from '@kbn/no-data-page-plugin/public'; +import { SharePluginStart } from '@kbn/share-plugin/public'; import type { IndexPatternManagementStart } from '.'; import type { DataViewMgmtService } from './management_app/data_view_management_service'; @@ -53,6 +54,7 @@ export interface IndexPatternManagmentContext extends StartServices { fieldFormatEditors: IndexPatternFieldEditorStart['fieldFormatEditors']; IndexPatternEditor: DataViewEditorStart['IndexPatternEditorComponent']; fieldFormats: FieldFormatsStart; + share?: SharePluginStart; spaces?: SpacesPluginStart; savedObjectsManagement: SavedObjectsManagementPluginStart; noDataPage?: NoDataPagePluginSetup; diff --git a/src/plugins/data_view_management/tsconfig.json b/src/plugins/data_view_management/tsconfig.json index ea0c96cc66b74..9857dd44829fa 100644 --- a/src/plugins/data_view_management/tsconfig.json +++ b/src/plugins/data_view_management/tsconfig.json @@ -45,6 +45,7 @@ "@kbn/code-editor", "@kbn/react-kibana-mount", "@kbn/rollup", + "@kbn/share-plugin", ], "exclude": [ "target/**/*", diff --git a/src/plugins/es_ui_shared/__packages_do_not_import__/ace/_ui_ace_keyboard_mode.scss b/src/plugins/es_ui_shared/__packages_do_not_import__/ace/_ui_ace_keyboard_mode.scss deleted file mode 100644 index 2ad92f3506b20..0000000000000 --- a/src/plugins/es_ui_shared/__packages_do_not_import__/ace/_ui_ace_keyboard_mode.scss +++ /dev/null @@ -1,24 +0,0 @@ -.kbnUiAceKeyboardHint { - position: absolute; - top: 0; - bottom: 0; - right: 0; - left: 0; - background: transparentize($euiColorEmptyShade, .3); - display: flex; - flex-direction: column; - justify-content: center; - align-items: center; - text-align: center; - opacity: 0; - - &:focus { - opacity: 1; - border: 2px solid $euiColorPrimary; - z-index: $euiZLevel1; - } - - &.kbnUiAceKeyboardHint-isInactive { - display: none; - } -} diff --git a/src/plugins/es_ui_shared/__packages_do_not_import__/ace/index.ts b/src/plugins/es_ui_shared/__packages_do_not_import__/ace/index.ts deleted file mode 100644 index 6214a2609462c..0000000000000 --- a/src/plugins/es_ui_shared/__packages_do_not_import__/ace/index.ts +++ /dev/null @@ -1,10 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the "Elastic License - * 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side - * Public License v 1"; you may not use this file except in compliance with, at - * your election, the "Elastic License 2.0", the "GNU Affero General Public - * License v3.0 only", or the "Server Side Public License, v 1". - */ - -export { useUIAceKeyboardMode } from './use_ui_ace_keyboard_mode'; diff --git a/src/plugins/es_ui_shared/__packages_do_not_import__/ace/use_ui_ace_keyboard_mode.tsx b/src/plugins/es_ui_shared/__packages_do_not_import__/ace/use_ui_ace_keyboard_mode.tsx deleted file mode 100644 index f1fe888104783..0000000000000 --- a/src/plugins/es_ui_shared/__packages_do_not_import__/ace/use_ui_ace_keyboard_mode.tsx +++ /dev/null @@ -1,113 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the "Elastic License - * 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side - * Public License v 1"; you may not use this file except in compliance with, at - * your election, the "Elastic License 2.0", the "GNU Affero General Public - * License v3.0 only", or the "Server Side Public License, v 1". - */ - -import React, { useEffect, useRef } from 'react'; -import * as ReactDOM from 'react-dom'; -import { keys, EuiText } from '@elastic/eui'; -import { KibanaRenderContextProvider } from '@kbn/react-kibana-context-render'; - -import './_ui_ace_keyboard_mode.scss'; -import type { AnalyticsServiceStart, I18nStart, ThemeServiceStart } from '@kbn/core/public'; - -interface StartServices { - analytics: Pick; - i18n: I18nStart; - theme: Pick; -} - -const OverlayText = (startServices: StartServices) => ( - // The point of this element is for accessibility purposes, so ignore eslint error - // in this case - // - - - Press Enter to start editing. - - When you’re done, press Escape to stop editing. - -); - -export function useUIAceKeyboardMode( - aceTextAreaElement: HTMLTextAreaElement | null, - startServices: StartServices, - isAccessibilityOverlayEnabled: boolean = true -) { - const overlayMountNode = useRef(null); - const autoCompleteVisibleRef = useRef(false); - useEffect(() => { - function onDismissOverlay(event: KeyboardEvent) { - if (event.key === keys.ENTER) { - event.preventDefault(); - aceTextAreaElement!.focus(); - } - } - - function enableOverlay() { - if (overlayMountNode.current) { - overlayMountNode.current.focus(); - } - } - - const isAutoCompleteVisible = () => { - const autoCompleter = document.querySelector('.ace_autocomplete'); - if (!autoCompleter) { - return false; - } - // The autoComplete is just hidden when it's closed, not removed from the DOM. - return autoCompleter.style.display !== 'none'; - }; - - const documentKeyDownListener = () => { - autoCompleteVisibleRef.current = isAutoCompleteVisible(); - }; - - const aceKeydownListener = (event: KeyboardEvent) => { - if (event.key === keys.ESCAPE && !autoCompleteVisibleRef.current) { - event.preventDefault(); - event.stopPropagation(); - enableOverlay(); - } - }; - if (aceTextAreaElement && isAccessibilityOverlayEnabled) { - // We don't control HTML elements inside of ace so we imperatively create an element - // that acts as a container and insert it just before ace's textarea element - // so that the overlay lives at the correct spot in the DOM hierarchy. - overlayMountNode.current = document.createElement('div'); - overlayMountNode.current.className = 'kbnUiAceKeyboardHint'; - overlayMountNode.current.setAttribute('role', 'application'); - overlayMountNode.current.tabIndex = 0; - overlayMountNode.current.addEventListener('focus', enableOverlay); - overlayMountNode.current.addEventListener('keydown', onDismissOverlay); - - ReactDOM.render(, overlayMountNode.current); - - aceTextAreaElement.parentElement!.insertBefore(overlayMountNode.current, aceTextAreaElement); - aceTextAreaElement.setAttribute('tabindex', '-1'); - - // Order of events: - // 1. Document capture event fires first and we check whether an autocomplete menu is open on keydown - // (not ideal because this is scoped to the entire document). - // 2. Ace changes it's state (like hiding or showing autocomplete menu) - // 3. We check what button was pressed and whether autocomplete was visible then determine - // whether it should act like a dismiss or if we should display an overlay. - document.addEventListener('keydown', documentKeyDownListener, { capture: true }); - aceTextAreaElement.addEventListener('keydown', aceKeydownListener); - } - return () => { - if (aceTextAreaElement && isAccessibilityOverlayEnabled) { - document.removeEventListener('keydown', documentKeyDownListener, { capture: true }); - aceTextAreaElement.removeEventListener('keydown', aceKeydownListener); - const textAreaContainer = aceTextAreaElement.parentElement; - if (textAreaContainer && textAreaContainer.contains(overlayMountNode.current!)) { - textAreaContainer.removeChild(overlayMountNode.current!); - } - } - }; - }, [aceTextAreaElement, startServices, isAccessibilityOverlayEnabled]); -} diff --git a/src/plugins/es_ui_shared/public/ace/index.ts b/src/plugins/es_ui_shared/public/ace/index.ts deleted file mode 100644 index 9d010117e560e..0000000000000 --- a/src/plugins/es_ui_shared/public/ace/index.ts +++ /dev/null @@ -1,10 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the "Elastic License - * 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side - * Public License v 1"; you may not use this file except in compliance with, at - * your election, the "Elastic License 2.0", the "GNU Affero General Public - * License v3.0 only", or the "Server Side Public License, v 1". - */ - -export { useUIAceKeyboardMode } from '../../__packages_do_not_import__/ace'; diff --git a/src/plugins/es_ui_shared/public/index.ts b/src/plugins/es_ui_shared/public/index.ts index 3b3ccc3fca08f..ddcdb84fa5758 100644 --- a/src/plugins/es_ui_shared/public/index.ts +++ b/src/plugins/es_ui_shared/public/index.ts @@ -12,7 +12,6 @@ * In the future, each top level folder should be exported like that to avoid naming collision */ import * as Forms from './forms'; -import * as ace from './ace'; import * as GlobalFlyout from './global_flyout'; import * as XJson from './xjson'; @@ -47,7 +46,7 @@ export { useAuthorizationContext, } from './authorization'; -export { Forms, ace, GlobalFlyout, XJson }; +export { Forms, GlobalFlyout, XJson }; export { extractQueryParams, attemptToURIDecode } from './url'; diff --git a/src/plugins/es_ui_shared/static/forms/components/index.ts b/src/plugins/es_ui_shared/static/forms/components/index.ts index 4ccfeed19dbfe..2e5dd03390eb7 100644 --- a/src/plugins/es_ui_shared/static/forms/components/index.ts +++ b/src/plugins/es_ui_shared/static/forms/components/index.ts @@ -7,22 +7,6 @@ * License v3.0 only", or the "Server Side Public License, v 1". */ -/* -@TODO - -The react-ace and brace/mode/json imports below are loaded eagerly - before this plugin is explicitly loaded by users. This makes -the brace JSON mode, used for JSON syntax highlighting and grammar checking, available across all of Kibana plugins. - -This is not ideal because we are loading JS that is not necessary for Kibana to start, but the alternative -is breaking JSON mode for an unknown number of ace editors across Kibana - not all components reference the underlying -EuiCodeEditor (for instance, explicitly). - -Importing here is a way of preventing a more sophisticated solution to this problem since we want to, eventually, -migrate all code editors over to Monaco. Once that is done, we should remove this import. - */ -import 'react-ace'; -import 'brace/mode/json'; - export * from './field'; export * from './form_row'; export * from './fields'; diff --git a/src/plugins/es_ui_shared/tsconfig.json b/src/plugins/es_ui_shared/tsconfig.json index f3dc3bb39a31d..2747f41b0f370 100644 --- a/src/plugins/es_ui_shared/tsconfig.json +++ b/src/plugins/es_ui_shared/tsconfig.json @@ -24,7 +24,6 @@ "@kbn/storybook", "@kbn/shared-ux-link-redirect-app", "@kbn/code-editor", - "@kbn/react-kibana-context-render", "@kbn/core-application-common", ], "exclude": [ diff --git a/src/plugins/esql/server/ui_settings.ts b/src/plugins/esql/server/ui_settings.ts index 39be07971769b..1ddae41c9b241 100644 --- a/src/plugins/esql/server/ui_settings.ts +++ b/src/plugins/esql/server/ui_settings.ts @@ -21,15 +21,7 @@ export const getUiSettings: () => Record = () => ({ value: true, description: i18n.translate('esql.advancedSettings.enableESQLDescription', { defaultMessage: - 'This setting enables ES|QL in Kibana. By switching it off you will hide the ES|QL user interface from various applications. However, users will be able to access existing ES|QL saved searches, visualizations, etc. If you have feedback on this experience please reach out to us on {link}', - values: { - link: - `` + - i18n.translate('esql.advancedSettings.enableESQL.discussLinkText', { - defaultMessage: 'https://ela.st/esql-feedback', - }) + - '', - }, + 'This setting enables ES|QL in Kibana. By switching it off you will hide the ES|QL user interface from various applications. However, users will be able to access existing ES|QL saved searches, visualizations, etc.', }), requiresPageReload: true, schema: schema.boolean(), 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 0ece5f004c23e..e5ddfbe4dd037 100644 --- a/src/plugins/kibana_usage_collection/server/collectors/management/schema.ts +++ b/src/plugins/kibana_usage_collection/server/collectors/management/schema.ts @@ -510,6 +510,10 @@ export const stackManagementSchema: MakeSchemaFrom = { _meta: { description: 'Non-default value of setting.' }, }, }, + 'observability:enableLogsStream': { + type: 'boolean', + _meta: { description: 'Non-default value of setting.' }, + }, 'banners:placement': { type: 'keyword', _meta: { description: 'Non-default value of setting.' }, @@ -701,4 +705,10 @@ export const stackManagementSchema: MakeSchemaFrom = { _meta: { description: 'Non-default value of setting.' }, }, }, + 'observability:newLogsOverview': { + type: 'boolean', + _meta: { + description: 'Enable the new logs overview component.', + }, + }, }; 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 ca1df3c95e87a..2acb487e7ed08 100644 --- a/src/plugins/kibana_usage_collection/server/collectors/management/types.ts +++ b/src/plugins/kibana_usage_collection/server/collectors/management/types.ts @@ -55,6 +55,8 @@ export interface UsageStats { 'observability:apmEnableServiceInventoryTableSearchBar': boolean; 'observability:logsExplorer:allowedDataViews': string[]; 'observability:logSources': string[]; + 'observability:enableLogsStream': boolean; + 'observability:newLogsOverview': boolean; 'observability:aiAssistantSimulatedFunctionCalling': boolean; 'observability:aiAssistantSearchConnectorIndexPattern': string; 'visualization:heatmap:maxBuckets': number; diff --git a/src/plugins/telemetry/schema/oss_plugins.json b/src/plugins/telemetry/schema/oss_plugins.json index f25c29e5f6952..830cffc17cf1c 100644 --- a/src/plugins/telemetry/schema/oss_plugins.json +++ b/src/plugins/telemetry/schema/oss_plugins.json @@ -10486,6 +10486,12 @@ } } }, + "observability:enableLogsStream": { + "type": "boolean", + "_meta": { + "description": "Non-default value of setting." + } + }, "banners:placement": { "type": "keyword", "_meta": { @@ -10762,6 +10768,12 @@ "description": "Non-default value of setting." } }, + "observability:newLogsOverview": { + "type": "boolean", + "_meta": { + "description": "Enable the new logs overview component." + } + }, "observability:searchExcludedDataTiers": { "type": "array", "items": { diff --git a/src/plugins/unified_histogram/public/services/lens_vis_service.suggestions.test.ts b/src/plugins/unified_histogram/public/services/lens_vis_service.suggestions.test.ts index 28819f7a5c54b..1719adebe7a49 100644 --- a/src/plugins/unified_histogram/public/services/lens_vis_service.suggestions.test.ts +++ b/src/plugins/unified_histogram/public/services/lens_vis_service.suggestions.test.ts @@ -12,6 +12,7 @@ import { DataViewField } from '@kbn/data-views-plugin/common'; import { deepMockedFields, buildDataViewMock } from '@kbn/discover-utils/src/__mocks__'; import { allSuggestionsMock } from '../__mocks__/suggestions'; import { getLensVisMock } from '../__mocks__/lens_vis'; +import { convertDatatableColumnToDataViewFieldSpec } from '@kbn/data-view-utils'; import { UnifiedHistogramSuggestionType } from '../types'; describe('LensVisService suggestions', () => { @@ -198,6 +199,11 @@ describe('LensVisService suggestions', () => { }); test('should return histogramSuggestion if no suggestions returned by the api with the breakdown field if it is given', async () => { + const breakdown = convertDatatableColumnToDataViewFieldSpec({ + name: 'var0', + id: 'var0', + meta: { type: 'number' }, + }); const lensVis = await getLensVisMock({ filters: [], query: { esql: 'from the-data-view | limit 100' }, @@ -207,7 +213,7 @@ describe('LensVisService suggestions', () => { from: '2023-09-03T08:00:00.000Z', to: '2023-09-04T08:56:28.274Z', }, - breakdownField: { name: 'var0' } as DataViewField, + breakdownField: breakdown as DataViewField, columns: [ { id: 'var0', @@ -247,4 +253,54 @@ describe('LensVisService suggestions', () => { expect(lensVis.visContext?.attributes.state.query).toStrictEqual(histogramQuery); }); + + test('should return histogramSuggestion if no suggestions returned by the api with a geo point breakdown field correctly', async () => { + const lensVis = await getLensVisMock({ + filters: [], + query: { esql: 'from the-data-view | limit 100' }, + dataView: dataViewMock, + timeInterval: 'auto', + timeRange: { + from: '2023-09-03T08:00:00.000Z', + to: '2023-09-04T08:56:28.274Z', + }, + breakdownField: { name: 'coordinates' } as DataViewField, + columns: [ + { + id: 'coordinates', + name: 'coordinates', + meta: { + type: 'geo_point', + }, + }, + ], + isPlainRecord: true, + allSuggestions: [], + hasHistogramSuggestionForESQL: true, + }); + + expect(lensVis.currentSuggestionContext?.type).toBe( + UnifiedHistogramSuggestionType.histogramForESQL + ); + expect(lensVis.currentSuggestionContext?.suggestion).toBeDefined(); + expect(lensVis.currentSuggestionContext?.suggestion?.visualizationState).toHaveProperty( + 'layers', + [ + { + layerId: '662552df-2cdc-4539-bf3b-73b9f827252c', + seriesType: 'bar_stacked', + xAccessor: '@timestamp every 30 second', + accessors: ['results'], + layerType: 'data', + }, + ] + ); + + const histogramQuery = { + esql: `from the-data-view | limit 100 +| EVAL timestamp=DATE_TRUNC(30 minute, @timestamp) | stats results = count(*) by timestamp, \`coordinates\` | rename timestamp as \`@timestamp every 30 minute\``, + }; + + expect(lensVis.visContext?.attributes.state.query).toStrictEqual(histogramQuery); + }); }); diff --git a/src/plugins/unified_histogram/public/services/lens_vis_service.ts b/src/plugins/unified_histogram/public/services/lens_vis_service.ts index eccfd663b2557..25bb8be6f6242 100644 --- a/src/plugins/unified_histogram/public/services/lens_vis_service.ts +++ b/src/plugins/unified_histogram/public/services/lens_vis_service.ts @@ -9,7 +9,11 @@ import { BehaviorSubject, distinctUntilChanged, map, Observable } from 'rxjs'; import { isEqual } from 'lodash'; -import { removeDropCommandsFromESQLQuery, appendToESQLQuery } from '@kbn/esql-utils'; +import { + removeDropCommandsFromESQLQuery, + appendToESQLQuery, + isESQLColumnSortable, +} from '@kbn/esql-utils'; import type { DataView, DataViewField } from '@kbn/data-views-plugin/common'; import type { CountIndexPatternColumn, @@ -553,12 +557,17 @@ export class LensVisService { const queryInterval = interval ?? computeInterval(timeRange, this.services.data); const language = getAggregateQueryMode(query); const safeQuery = removeDropCommandsFromESQLQuery(query[language]); - const breakdown = breakdownColumn - ? `, \`${breakdownColumn.name}\` | sort \`${breakdownColumn.name}\` asc` - : ''; + const breakdown = breakdownColumn ? `, \`${breakdownColumn.name}\`` : ''; + + // sort by breakdown column if it's sortable + const sortBy = + breakdownColumn && isESQLColumnSortable(breakdownColumn) + ? ` | sort \`${breakdownColumn.name}\` asc` + : ''; + return appendToESQLQuery( safeQuery, - `| EVAL timestamp=DATE_TRUNC(${queryInterval}, ${dataView.timeFieldName}) | stats results = count(*) by timestamp${breakdown} | rename timestamp as \`${dataView.timeFieldName} every ${queryInterval}\`` + `| EVAL timestamp=DATE_TRUNC(${queryInterval}, ${dataView.timeFieldName}) | stats results = count(*) by timestamp${breakdown}${sortBy} | rename timestamp as \`${dataView.timeFieldName} every ${queryInterval}\`` ); }; diff --git a/src/plugins/usage_collection/server/usage_counters/saved_objects.test.ts b/src/plugins/usage_collection/server/usage_counters/saved_objects.test.ts index ebced92622779..927869b6d0f89 100644 --- a/src/plugins/usage_collection/server/usage_counters/saved_objects.test.ts +++ b/src/plugins/usage_collection/server/usage_counters/saved_objects.test.ts @@ -80,6 +80,7 @@ describe('storeCounter', () => { ], Object { "namespace": "default", + "refresh": false, "upsertAttributes": Object { "counterName": "b", "counterType": "c", diff --git a/src/plugins/usage_collection/server/usage_counters/saved_objects.ts b/src/plugins/usage_collection/server/usage_counters/saved_objects.ts index 9c4e2832946e6..d5f49016e5296 100644 --- a/src/plugins/usage_collection/server/usage_counters/saved_objects.ts +++ b/src/plugins/usage_collection/server/usage_counters/saved_objects.ts @@ -122,6 +122,7 @@ export const storeCounter = async ({ metric, soRepository }: StoreCounterParams) counterType, source, }, + refresh: false, } ); }; diff --git a/src/plugins/usage_collection/server/usage_counters/usage_counters_service.test.ts b/src/plugins/usage_collection/server/usage_counters/usage_counters_service.test.ts index 6128b643918a1..1041cfb5ce36f 100644 --- a/src/plugins/usage_collection/server/usage_counters/usage_counters_service.test.ts +++ b/src/plugins/usage_collection/server/usage_counters/usage_counters_service.test.ts @@ -157,6 +157,7 @@ describe('UsageCountersService', () => { }, ], Object { + "refresh": false, "upsertAttributes": Object { "counterName": "counterA", "counterType": "count", @@ -175,6 +176,7 @@ describe('UsageCountersService', () => { }, ], Object { + "refresh": false, "upsertAttributes": Object { "counterName": "counterB", "counterType": "count", diff --git a/src/plugins/vis_default_editor/public/default_editor.tsx b/src/plugins/vis_default_editor/public/default_editor.tsx index f61450c8e85e0..dc9e83e8c3b43 100644 --- a/src/plugins/vis_default_editor/public/default_editor.tsx +++ b/src/plugins/vis_default_editor/public/default_editor.tsx @@ -8,7 +8,6 @@ */ import './index.scss'; -import 'brace/mode/json'; import React, { useEffect, useRef, useState, useCallback } from 'react'; import { EventEmitter } from 'events'; diff --git a/test/functional/apps/dashboard/group1/create_and_add_embeddables.ts b/test/functional/apps/dashboard/group1/create_and_add_embeddables.ts index 61929b19ff468..34e78f3e68632 100644 --- a/test/functional/apps/dashboard/group1/create_and_add_embeddables.ts +++ b/test/functional/apps/dashboard/group1/create_and_add_embeddables.ts @@ -8,7 +8,7 @@ */ import expect from '@kbn/expect'; - +import { OBSERVABILITY_ENABLE_LOGS_STREAM } from '@kbn/management-settings-ids'; import { VisualizeConstants } from '@kbn/visualizations-plugin/common/constants'; import { FtrProviderContext } from '../../../ftr_provider_context'; @@ -28,6 +28,12 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { await kibanaServer.uiSettings.replace({ defaultIndex: '0bf35f60-3dc9-11e8-8660-4d65aa086b3c', }); + await kibanaServer.uiSettings.update({ [OBSERVABILITY_ENABLE_LOGS_STREAM]: true }); + }); + + after(async () => { + await kibanaServer.savedObjects.cleanStandardList(); + await kibanaServer.uiSettings.update({ [OBSERVABILITY_ENABLE_LOGS_STREAM]: false }); }); it('ensure toolbar popover closes on add', async () => { @@ -39,10 +45,6 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { await dashboardAddPanel.expectEditorMenuClosed(); }); - after(async () => { - await kibanaServer.savedObjects.cleanStandardList(); - }); - describe('add new visualization link', () => { before(async () => { await dashboard.navigateToApp(); diff --git a/test/functional/apps/dashboard/group6/dashboard_esql_no_data.ts b/test/functional/apps/dashboard/group6/dashboard_esql_no_data.ts new file mode 100644 index 0000000000000..148cb95a82b11 --- /dev/null +++ b/test/functional/apps/dashboard/group6/dashboard_esql_no_data.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", the "GNU Affero General Public License v3.0 only", and the "Server Side + * Public License v 1"; you may not use this file except in compliance with, at + * your election, the "Elastic License 2.0", the "GNU Affero General Public + * License v3.0 only", or the "Server Side Public License, v 1". + */ + +import { FtrProviderContext } from '../../../ftr_provider_context'; + +export default function ({ getService, getPageObjects }: FtrProviderContext) { + const kibanaServer = getService('kibanaServer'); + const testSubjects = getService('testSubjects'); + const esql = getService('esql'); + const PageObjects = getPageObjects(['discover', 'dashboard']); + + describe('No Data Views: Try ES|QL', () => { + before(async () => { + await kibanaServer.savedObjects.cleanStandardList(); + }); + + it('enables user to create a dashboard with ES|QL from no-data-prompt', async () => { + await PageObjects.dashboard.navigateToApp(); + + await testSubjects.existOrFail('noDataViewsPrompt'); + await testSubjects.click('tryESQLLink'); + + await PageObjects.discover.expectOnDiscover(); + await esql.expectEsqlStatement('FROM logs* | LIMIT 10'); + }); + }); +} diff --git a/test/functional/apps/dashboard/group6/index.ts b/test/functional/apps/dashboard/group6/index.ts index 302ca2e0480a0..340c9b425571b 100644 --- a/test/functional/apps/dashboard/group6/index.ts +++ b/test/functional/apps/dashboard/group6/index.ts @@ -37,5 +37,6 @@ export default function ({ getService, loadTestFile }: FtrProviderContext) { loadTestFile(require.resolve('./dashboard_snapshots')); loadTestFile(require.resolve('./embeddable_library')); loadTestFile(require.resolve('./dashboard_esql_chart')); + loadTestFile(require.resolve('./dashboard_esql_no_data')); }); } diff --git a/test/functional/apps/management/data_views/_scripted_fields.ts b/test/functional/apps/management/data_views/_scripted_fields.ts index 172537bf4e73a..f86ae72aa5047 100644 --- a/test/functional/apps/management/data_views/_scripted_fields.ts +++ b/test/functional/apps/management/data_views/_scripted_fields.ts @@ -19,10 +19,6 @@ // 3. Filter in Discover by the scripted field // 4. Visualize with aggregation on the scripted field by clicking unifiedFieldList.clickFieldListItemVisualize -// NOTE: Scripted field input is managed by Ace editor, which automatically -// appends closing braces, for exmaple, if you type opening square brace [ -// it will automatically insert a a closing square brace ], etc. - import expect from '@kbn/expect'; import { FtrProviderContext } from '../../../ftr_provider_context'; diff --git a/test/functional/apps/management/data_views/_scripted_fields_classic_table.ts b/test/functional/apps/management/data_views/_scripted_fields_classic_table.ts index 063e0b960d52e..4f3d30222e496 100644 --- a/test/functional/apps/management/data_views/_scripted_fields_classic_table.ts +++ b/test/functional/apps/management/data_views/_scripted_fields_classic_table.ts @@ -19,10 +19,6 @@ // 3. Filter in Discover by the scripted field // 4. Visualize with aggregation on the scripted field by clicking unifiedFieldList.clickFieldListItemVisualize -// NOTE: Scripted field input is managed by Ace editor, which automatically -// appends closing braces, for exmaple, if you type opening square brace [ -// it will automatically insert a a closing square brace ], etc. - import expect from '@kbn/expect'; import { FtrProviderContext } from '../../../ftr_provider_context'; diff --git a/test/functional/apps/management/data_views/_try_esql.ts b/test/functional/apps/management/data_views/_try_esql.ts new file mode 100644 index 0000000000000..276e61c4a721f --- /dev/null +++ b/test/functional/apps/management/data_views/_try_esql.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", the "GNU Affero General Public License v3.0 only", and the "Server Side + * Public License v 1"; you may not use this file except in compliance with, at + * your election, the "Elastic License 2.0", the "GNU Affero General Public + * License v3.0 only", or the "Server Side Public License, v 1". + */ + +import { FtrProviderContext } from '../../../ftr_provider_context'; + +export default function ({ getService, getPageObjects }: FtrProviderContext) { + const kibanaServer = getService('kibanaServer'); + const testSubjects = getService('testSubjects'); + const esql = getService('esql'); + const PageObjects = getPageObjects(['settings', 'common', 'discover']); + + describe('No Data Views: Try ES|QL', () => { + before(async () => { + await kibanaServer.savedObjects.cleanStandardList(); + }); + + it('navigates to Discover and presents an ES|QL query', async () => { + await PageObjects.settings.navigateTo(); + await PageObjects.settings.clickKibanaIndexPatterns(); + + await testSubjects.existOrFail('noDataViewsPrompt'); + await testSubjects.click('tryESQLLink'); + + await PageObjects.discover.expectOnDiscover(); + await esql.expectEsqlStatement('FROM logs* | LIMIT 10'); + }); + }); +} diff --git a/test/functional/apps/management/index.ts b/test/functional/apps/management/index.ts index f3d26f2e1c6d7..2300543f06d51 100644 --- a/test/functional/apps/management/index.ts +++ b/test/functional/apps/management/index.ts @@ -38,6 +38,7 @@ export default function ({ getService, loadTestFile }: FtrProviderContext) { loadTestFile(require.resolve('./data_views/_legacy_url_redirect')); loadTestFile(require.resolve('./data_views/_exclude_index_pattern')); loadTestFile(require.resolve('./data_views/_index_pattern_filter')); + loadTestFile(require.resolve('./data_views/_try_esql')); loadTestFile(require.resolve('./data_views/_scripted_fields_filter')); loadTestFile(require.resolve('./_import_objects')); loadTestFile(require.resolve('./data_views/_test_huge_fields')); diff --git a/test/functional/page_objects/discover_page.ts b/test/functional/page_objects/discover_page.ts index 1474e9d315538..ab6356075fd81 100644 --- a/test/functional/page_objects/discover_page.ts +++ b/test/functional/page_objects/discover_page.ts @@ -32,6 +32,12 @@ export class DiscoverPageObject extends FtrService { private readonly defaultFindTimeout = this.config.get('timeouts.find'); + /** Ensures that navigation to discover has completed */ + public async expectOnDiscover() { + await this.testSubjects.existOrFail('discoverNewButton'); + await this.testSubjects.existOrFail('discoverOpenButton'); + } + public async getChartTimespan() { return await this.testSubjects.getAttribute('unifiedHistogramChart', 'data-time-range'); } diff --git a/test/functional/page_objects/solution_navigation.ts b/test/functional/page_objects/solution_navigation.ts index a0544e1100507..79e13b0f24943 100644 --- a/test/functional/page_objects/solution_navigation.ts +++ b/test/functional/page_objects/solution_navigation.ts @@ -138,6 +138,18 @@ export function SolutionNavigationProvider(ctx: Pick { const queryHistory = await this.testSubjects.find('ESQLEditor-queryHistory'); const tableBody = await this.retry.try(async () => queryHistory.findByTagName('tbody')); diff --git a/test/interactive_setup_functional/tests/manual_configuration.ts b/test/interactive_setup_functional/tests/manual_configuration.ts index 8d3956f1cd3c6..59b2391d27f54 100644 --- a/test/interactive_setup_functional/tests/manual_configuration.ts +++ b/test/interactive_setup_functional/tests/manual_configuration.ts @@ -8,6 +8,7 @@ */ import { getUrl, kibanaServerTestUser } from '@kbn/test'; + import type { FtrProviderContext } from '../../functional/ftr_provider_context'; export default function ({ getService }: FtrProviderContext) { 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 af95c916622b7..e1fbb205dc53a 100644 --- a/test/interactive_setup_functional/tests/manual_configuration_without_security.ts +++ b/test/interactive_setup_functional/tests/manual_configuration_without_security.ts @@ -8,6 +8,7 @@ */ import { getUrl } from '@kbn/test'; + import type { FtrProviderContext } from '../../functional/ftr_provider_context'; export default function ({ getService, getPageObject }: FtrProviderContext) { 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 bd5bd3c5f33ab..666cc1cf05290 100644 --- a/test/interactive_setup_functional/tests/manual_configuration_without_tls.ts +++ b/test/interactive_setup_functional/tests/manual_configuration_without_tls.ts @@ -8,6 +8,7 @@ */ import { getUrl, kibanaServerTestUser } from '@kbn/test'; + import type { FtrProviderContext } from '../../functional/ftr_provider_context'; export default function ({ getService }: FtrProviderContext) { diff --git a/test/plugin_functional/test_suites/core_plugins/rendering.ts b/test/plugin_functional/test_suites/core_plugins/rendering.ts index 0054750a55b24..6c9d805c43b30 100644 --- a/test/plugin_functional/test_suites/core_plugins/rendering.ts +++ b/test/plugin_functional/test_suites/core_plugins/rendering.ts @@ -315,6 +315,8 @@ export default function ({ getService }: PluginFunctionalProviderContext) { // 'xpack.reporting.poll.jobsRefresh.intervalErrorMultiplier (number)', 'xpack.rollup.ui.enabled (boolean?)', 'xpack.saved_object_tagging.cache_refresh_interval (duration?)', + + 'xpack.searchAssistant.ui.enabled (boolean?)', 'xpack.searchInferenceEndpoints.ui.enabled (boolean?)', 'xpack.searchPlayground.ui.enabled (boolean?)', 'xpack.security.loginAssistanceMessage (string?)', diff --git a/test/tsconfig.json b/test/tsconfig.json index 8b0d946bded62..1d8c301c44a2b 100644 --- a/test/tsconfig.json +++ b/test/tsconfig.json @@ -76,5 +76,6 @@ "@kbn/default-nav-management", "@kbn/default-nav-devtools", "@kbn/core-saved-objects-import-export-server-internal", + "@kbn/management-settings-ids", ] } diff --git a/tsconfig.base.json b/tsconfig.base.json index 3df30d9cf8c30..188c96734d2ce 100644 --- a/tsconfig.base.json +++ b/tsconfig.base.json @@ -6,8 +6,6 @@ // START AUTOMATED PACKAGE LISTING "@kbn/aad-fixtures-plugin": ["x-pack/test/alerting_api_integration/common/plugins/aad"], "@kbn/aad-fixtures-plugin/*": ["x-pack/test/alerting_api_integration/common/plugins/aad/*"], - "@kbn/ace": ["packages/kbn-ace"], - "@kbn/ace/*": ["packages/kbn-ace/*"], "@kbn/actions-plugin": ["x-pack/plugins/actions"], "@kbn/actions-plugin/*": ["x-pack/plugins/actions/*"], "@kbn/actions-simulators-plugin": ["x-pack/test/alerting_api_integration/common/plugins/actions_simulators"], @@ -16,6 +14,8 @@ "@kbn/actions-types/*": ["packages/kbn-actions-types/*"], "@kbn/advanced-settings-plugin": ["src/plugins/advanced_settings"], "@kbn/advanced-settings-plugin/*": ["src/plugins/advanced_settings/*"], + "@kbn/ai-assistant": ["x-pack/packages/kbn-ai-assistant"], + "@kbn/ai-assistant/*": ["x-pack/packages/kbn-ai-assistant/*"], "@kbn/ai-assistant-management-plugin": ["src/plugins/ai_assistant_management/selection"], "@kbn/ai-assistant-management-plugin/*": ["src/plugins/ai_assistant_management/selection/*"], "@kbn/aiops-change-point-detection": ["x-pack/packages/ml/aiops_change_point_detection"], @@ -1298,6 +1298,8 @@ "@kbn/observability-get-padded-alert-time-range-util/*": ["x-pack/packages/observability/get_padded_alert_time_range_util/*"], "@kbn/observability-logs-explorer-plugin": ["x-pack/plugins/observability_solution/observability_logs_explorer"], "@kbn/observability-logs-explorer-plugin/*": ["x-pack/plugins/observability_solution/observability_logs_explorer/*"], + "@kbn/observability-logs-overview": ["x-pack/packages/observability/logs_overview"], + "@kbn/observability-logs-overview/*": ["x-pack/packages/observability/logs_overview/*"], "@kbn/observability-onboarding-e2e": ["x-pack/plugins/observability_solution/observability_onboarding/e2e"], "@kbn/observability-onboarding-e2e/*": ["x-pack/plugins/observability_solution/observability_onboarding/e2e/*"], "@kbn/observability-onboarding-plugin": ["x-pack/plugins/observability_solution/observability_onboarding"], diff --git a/updatecli-compose.yaml b/updatecli-compose.yaml new file mode 100644 index 0000000000000..8ad9bd6df8afb --- /dev/null +++ b/updatecli-compose.yaml @@ -0,0 +1,14 @@ +# Config file for `updatecli compose ...`. +# https://www.updatecli.io/docs/core/compose/ +policies: + - name: Handle ironbank bumps + policy: ghcr.io/elastic/oblt-updatecli-policies/ironbank/templates:0.3.0@sha256:b0c841d8fb294e6b58359462afbc83070dca375ac5dd0c5216c8926872a98bb1 + values: + - .github/updatecli/values.d/scm.yml + - .github/updatecli/values.d/ironbank.yml + + - name: Update Updatecli policies + policy: ghcr.io/updatecli/policies/autodiscovery/updatecli:0.4.0@sha256:254367f5b1454fd6032b88b314450cd3b6d5e8d5b6c953eb242a6464105eb869 + values: + - .github/updatecli/values.d/scm.yml + - .github/updatecli/values.d/updatecli-compose.yml \ No newline at end of file diff --git a/x-pack/.i18nrc.json b/x-pack/.i18nrc.json index a46e291093411..7afbc9dc704c4 100644 --- a/x-pack/.i18nrc.json +++ b/x-pack/.i18nrc.json @@ -8,6 +8,7 @@ "packages/ml/aiops_log_rate_analysis", "plugins/aiops" ], + "xpack.aiAssistant": "packages/kbn-ai-assistant", "xpack.alerting": "plugins/alerting", "xpack.eventLog": "plugins/event_log", "xpack.stackAlerts": "plugins/stack_alerts", @@ -44,9 +45,15 @@ "xpack.dataVisualizer": "plugins/data_visualizer", "xpack.exploratoryView": "plugins/observability_solution/exploratory_view", "xpack.fileUpload": "plugins/file_upload", - "xpack.globalSearch": ["plugins/global_search"], - "xpack.globalSearchBar": ["plugins/global_search_bar"], - "xpack.graph": ["plugins/graph"], + "xpack.globalSearch": [ + "plugins/global_search" + ], + "xpack.globalSearchBar": [ + "plugins/global_search_bar" + ], + "xpack.graph": [ + "plugins/graph" + ], "xpack.grokDebugger": "plugins/grokdebugger", "xpack.idxMgmt": "plugins/index_management", "xpack.idxMgmtPackage": "packages/index-management", @@ -68,9 +75,13 @@ "xpack.licenseMgmt": "plugins/license_management", "xpack.licensing": "plugins/licensing", "xpack.lists": "plugins/lists", - "xpack.logstash": ["plugins/logstash"], + "xpack.logstash": [ + "plugins/logstash" + ], "xpack.main": "legacy/plugins/xpack_main", - "xpack.maps": ["plugins/maps"], + "xpack.maps": [ + "plugins/maps" + ], "xpack.metricsData": "plugins/observability_solution/metrics_data_access", "xpack.ml": [ "packages/ml/anomaly_utils", @@ -85,7 +96,9 @@ "packages/ml/ui_actions", "plugins/ml" ], - "xpack.monitoring": ["plugins/monitoring"], + "xpack.monitoring": [ + "plugins/monitoring" + ], "xpack.observability": "plugins/observability_solution/observability", "xpack.observabilityAiAssistant": [ "plugins/observability_solution/observability_ai_assistant", @@ -95,12 +108,22 @@ "xpack.observabilityLogsExplorer": "plugins/observability_solution/observability_logs_explorer", "xpack.observability_onboarding": "plugins/observability_solution/observability_onboarding", "xpack.observabilityShared": "plugins/observability_solution/observability_shared", + "xpack.observabilityLogsOverview": [ + "packages/observability/logs_overview/src/components" + ], "xpack.osquery": ["plugins/osquery"], "xpack.painlessLab": "plugins/painless_lab", - "xpack.profiling": ["plugins/observability_solution/profiling"], + "xpack.profiling": [ + "plugins/observability_solution/profiling" + ], "xpack.remoteClusters": "plugins/remote_clusters", - "xpack.reporting": ["plugins/reporting"], - "xpack.rollupJobs": ["packages/rollup", "plugins/rollup"], + "xpack.reporting": [ + "plugins/reporting" + ], + "xpack.rollupJobs": [ + "packages/rollup", + "plugins/rollup" + ], "xpack.runtimeFields": "plugins/runtime_fields", "xpack.screenshotting": "plugins/screenshotting", "xpack.searchSharedUI": "packages/search/shared_ui", @@ -111,7 +134,10 @@ "xpack.searchInferenceEndpoints": "plugins/search_inference_endpoints", "xpack.searchAssistant": "plugins/search_assistant", "xpack.searchProfiler": "plugins/searchprofiler", - "xpack.security": ["plugins/security", "packages/security"], + "xpack.security": [ + "plugins/security", + "packages/security" + ], "xpack.server": "legacy/server", "xpack.serverless": "plugins/serverless", "xpack.serverlessSearch": "plugins/serverless_search", @@ -123,20 +149,30 @@ "xpack.slo": "plugins/observability_solution/slo", "xpack.snapshotRestore": "plugins/snapshot_restore", "xpack.spaces": "plugins/spaces", - "xpack.savedObjectsTagging": ["plugins/saved_objects_tagging"], + "xpack.savedObjectsTagging": [ + "plugins/saved_objects_tagging" + ], "xpack.taskManager": "legacy/plugins/task_manager", "xpack.threatIntelligence": "plugins/threat_intelligence", "xpack.timelines": "plugins/timelines", "xpack.transform": "plugins/transform", "xpack.triggersActionsUI": "plugins/triggers_actions_ui", "xpack.upgradeAssistant": "plugins/upgrade_assistant", - "xpack.uptime": ["plugins/observability_solution/uptime"], - "xpack.synthetics": ["plugins/observability_solution/synthetics"], - "xpack.ux": ["plugins/observability_solution/ux"], + "xpack.uptime": [ + "plugins/observability_solution/uptime" + ], + "xpack.synthetics": [ + "plugins/observability_solution/synthetics" + ], + "xpack.ux": [ + "plugins/observability_solution/ux" + ], "xpack.urlDrilldown": "plugins/drilldowns/url_drilldown", "xpack.watcher": "plugins/watcher" }, - "exclude": ["examples"], + "exclude": [ + "examples" + ], "translations": [ "@kbn/translations-plugin/translations/zh-CN.json", "@kbn/translations-plugin/translations/ja-JP.json", diff --git a/x-pack/packages/kbn-ai-assistant/README.md b/x-pack/packages/kbn-ai-assistant/README.md new file mode 100644 index 0000000000000..d28f93431baa9 --- /dev/null +++ b/x-pack/packages/kbn-ai-assistant/README.md @@ -0,0 +1,3 @@ +# @kbn/ai-assistant + +Provides components, types and context to render the AI Assistant in plugins. diff --git a/x-pack/packages/kbn-elastic-assistant/impl/knowledge_base/const.ts b/x-pack/packages/kbn-ai-assistant/index.ts similarity index 53% rename from x-pack/packages/kbn-elastic-assistant/impl/knowledge_base/const.ts rename to x-pack/packages/kbn-ai-assistant/index.ts index 3cfd0cf3b4205..cf53082cfa4b0 100644 --- a/x-pack/packages/kbn-elastic-assistant/impl/knowledge_base/const.ts +++ b/x-pack/packages/kbn-ai-assistant/index.ts @@ -4,6 +4,4 @@ * 2.0; you may not use this file except in compliance with the Elastic License * 2.0. */ -export const ESQL_RESOURCE = 'esql'; -export const KNOWLEDGE_BASE_INDEX_PATTERN_OLD = '.kibana-elastic-ai-assistant-kb'; -export const KNOWLEDGE_BASE_INDEX_PATTERN = '.kibana-elastic-ai-assistant-knowledge-base-(SPACE)'; +export * from './src'; diff --git a/x-pack/packages/kbn-ai-assistant/jest.config.js b/x-pack/packages/kbn-ai-assistant/jest.config.js new file mode 100644 index 0000000000000..37d30bae01fa9 --- /dev/null +++ b/x-pack/packages/kbn-ai-assistant/jest.config.js @@ -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. + */ + +module.exports = { + coverageDirectory: '/target/kibana-coverage/jest/x-pack/packages/kbn_ai_assistant_src', + coverageReporters: ['text', 'html'], + collectCoverageFrom: [ + '/x-pack/packages/kbn-ai-assistant/src/**/*.{ts,tsx}', + '!/x-pack/packages/kbn-ai-assistant/src/*.test.{ts,tsx}', + ], + preset: '@kbn/test', + rootDir: '../../..', + roots: ['/x-pack/packages/kbn-ai-assistant'], +}; diff --git a/x-pack/packages/kbn-ai-assistant/kibana.jsonc b/x-pack/packages/kbn-ai-assistant/kibana.jsonc new file mode 100644 index 0000000000000..4cddd90431e39 --- /dev/null +++ b/x-pack/packages/kbn-ai-assistant/kibana.jsonc @@ -0,0 +1,5 @@ +{ + "id": "@kbn/ai-assistant", + "owner": "@elastic/search-kibana", + "type": "shared-browser" +} diff --git a/x-pack/packages/kbn-ai-assistant/package.json b/x-pack/packages/kbn-ai-assistant/package.json new file mode 100644 index 0000000000000..159ed64f288fd --- /dev/null +++ b/x-pack/packages/kbn-ai-assistant/package.json @@ -0,0 +1,7 @@ +{ + "name": "@kbn/ai-assistant", + "private": true, + "version": "1.0.0", + "license": "Elastic License 2.0", + "sideEffects": false +} diff --git a/x-pack/packages/kbn-ai-assistant/setup_tests.ts b/x-pack/packages/kbn-ai-assistant/setup_tests.ts new file mode 100644 index 0000000000000..72e0edd0d07f7 --- /dev/null +++ b/x-pack/packages/kbn-ai-assistant/setup_tests.ts @@ -0,0 +1,9 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +// eslint-disable-next-line import/no-extraneous-dependencies +import '@testing-library/jest-dom'; diff --git a/x-pack/packages/kbn-ai-assistant/src/assets/elastic_ai_assistant.png b/x-pack/packages/kbn-ai-assistant/src/assets/elastic_ai_assistant.png new file mode 100644 index 0000000000000..af10645579683 Binary files /dev/null and b/x-pack/packages/kbn-ai-assistant/src/assets/elastic_ai_assistant.png differ diff --git a/x-pack/plugins/observability_solution/observability_ai_assistant_app/public/components/buttons/ask_assistant_button.stories.tsx b/x-pack/packages/kbn-ai-assistant/src/buttons/ask_assistant_button.stories.tsx similarity index 100% rename from x-pack/plugins/observability_solution/observability_ai_assistant_app/public/components/buttons/ask_assistant_button.stories.tsx rename to x-pack/packages/kbn-ai-assistant/src/buttons/ask_assistant_button.stories.tsx diff --git a/x-pack/plugins/observability_solution/observability_ai_assistant_app/public/components/buttons/ask_assistant_button.tsx b/x-pack/packages/kbn-ai-assistant/src/buttons/ask_assistant_button.tsx similarity index 71% rename from x-pack/plugins/observability_solution/observability_ai_assistant_app/public/components/buttons/ask_assistant_button.tsx rename to x-pack/packages/kbn-ai-assistant/src/buttons/ask_assistant_button.tsx index e13ba34110434..624c3df9a1e84 100644 --- a/x-pack/plugins/observability_solution/observability_ai_assistant_app/public/components/buttons/ask_assistant_button.tsx +++ b/x-pack/packages/kbn-ai-assistant/src/buttons/ask_assistant_button.tsx @@ -46,12 +46,13 @@ export function AskAssistantButton({ variant, onClick, }: AskAssistantButtonProps) { - const buttonLabel = i18n.translate( - 'xpack.observabilityAiAssistant.askAssistantButton.buttonLabel', - { - defaultMessage: 'Ask Assistant', - } - ); + const buttonLabel = i18n.translate('xpack.aiAssistant.askAssistantButton.buttonLabel', { + defaultMessage: 'Ask Assistant', + }); + + const aiAssistantLabel = i18n.translate('xpack.aiAssistant.aiAssistantLabel', { + defaultMessage: 'AI Assistant', + }); switch (variant) { case 'basic': @@ -84,23 +85,13 @@ export function AskAssistantButton({ return ( {props.isExpanded - ? i18n.translate('xpack.observabilityAiAssistant.hideExpandConversationButton.hide', { + ? i18n.translate('xpack.aiAssistant.hideExpandConversationButton.hide', { defaultMessage: 'Hide chats', }) - : i18n.translate('xpack.observabilityAiAssistant.hideExpandConversationButton.show', { + : i18n.translate('xpack.aiAssistant.hideExpandConversationButton.show', { defaultMessage: 'Show chats', })} diff --git a/x-pack/plugins/observability_solution/observability_ai_assistant_app/public/components/buttons/new_chat_button.stories.tsx b/x-pack/packages/kbn-ai-assistant/src/buttons/new_chat_button.stories.tsx similarity index 100% rename from x-pack/plugins/observability_solution/observability_ai_assistant_app/public/components/buttons/new_chat_button.stories.tsx rename to x-pack/packages/kbn-ai-assistant/src/buttons/new_chat_button.stories.tsx diff --git a/x-pack/plugins/observability_solution/observability_ai_assistant_app/public/components/buttons/new_chat_button.tsx b/x-pack/packages/kbn-ai-assistant/src/buttons/new_chat_button.tsx similarity index 92% rename from x-pack/plugins/observability_solution/observability_ai_assistant_app/public/components/buttons/new_chat_button.tsx rename to x-pack/packages/kbn-ai-assistant/src/buttons/new_chat_button.tsx index 75cede6344c59..3e515e87c2197 100644 --- a/x-pack/plugins/observability_solution/observability_ai_assistant_app/public/components/buttons/new_chat_button.tsx +++ b/x-pack/packages/kbn-ai-assistant/src/buttons/new_chat_button.tsx @@ -18,7 +18,7 @@ export function NewChatButton( iconType="newChat" {...nextProps} > - {i18n.translate('xpack.observabilityAiAssistant.newChatButton', { + {i18n.translate('xpack.aiAssistant.newChatButton', { defaultMessage: 'New chat', })} diff --git a/x-pack/plugins/observability_solution/observability_ai_assistant_app/public/components/chat/chat_actions_menu.tsx b/x-pack/packages/kbn-ai-assistant/src/chat/chat_actions_menu.tsx similarity index 65% rename from x-pack/plugins/observability_solution/observability_ai_assistant_app/public/components/chat/chat_actions_menu.tsx rename to x-pack/packages/kbn-ai-assistant/src/chat/chat_actions_menu.tsx index 713a0d2311e3c..ac25fe6c3703a 100644 --- a/x-pack/plugins/observability_solution/observability_ai_assistant_app/public/components/chat/chat_actions_menu.tsx +++ b/x-pack/packages/kbn-ai-assistant/src/chat/chat_actions_menu.tsx @@ -16,10 +16,8 @@ import { EuiToolTip, } from '@elastic/eui'; import { ConnectorSelectorBase } from '@kbn/observability-ai-assistant-plugin/public'; -import { useKibana } from '../../hooks/use_kibana'; -import { getSettingsHref } from '../../utils/get_settings_href'; -import { getSettingsKnowledgeBaseHref } from '../../utils/get_settings_kb_href'; -import type { UseGenAIConnectorsResult } from '../../hooks/use_genai_connectors'; +import type { UseGenAIConnectorsResult } from '../hooks/use_genai_connectors'; +import { useKibana } from '../hooks/use_kibana'; export function ChatActionsMenu({ connectors, @@ -32,14 +30,11 @@ export function ChatActionsMenu({ disabled: boolean; onCopyConversationClick: () => void; }) { - const { - application: { navigateToUrl, navigateToApp }, - http, - } = useKibana().services; + const { application, http } = useKibana().services; const [isOpen, setIsOpen] = useState(false); const handleNavigateToConnectors = () => { - navigateToApp('management', { + application?.navigateToApp('management', { path: '/insightsAndAlerting/triggersActionsConnectors/connectors', }); }; @@ -49,11 +44,17 @@ export function ChatActionsMenu({ }; const handleNavigateToSettings = () => { - navigateToUrl(getSettingsHref(http)); + application?.navigateToUrl( + http!.basePath.prepend(`/app/management/kibana/observabilityAiAssistantManagement`) + ); }; const handleNavigateToSettingsKnowledgeBase = () => { - navigateToUrl(getSettingsKnowledgeBaseHref(http)); + application?.navigateToUrl( + http!.basePath.prepend( + `/app/management/kibana/observabilityAiAssistantManagement?tab=knowledge_base` + ) + ); }; return ( @@ -61,10 +62,9 @@ export function ChatActionsMenu({ isOpen={isOpen} button={ @@ -87,24 +87,21 @@ export function ChatActionsMenu({ panels={[ { id: 0, - title: i18n.translate('xpack.observabilityAiAssistant.chatHeader.actions.title', { + title: i18n.translate('xpack.aiAssistant.chatHeader.actions.title', { defaultMessage: 'Actions', }), items: [ { - name: i18n.translate( - 'xpack.observabilityAiAssistant.chatHeader.actions.knowledgeBase', - { - defaultMessage: 'Manage knowledge base', - } - ), + name: i18n.translate('xpack.aiAssistant.chatHeader.actions.knowledgeBase', { + defaultMessage: 'Manage knowledge base', + }), onClick: () => { toggleActionsMenu(); handleNavigateToSettingsKnowledgeBase(); }, }, { - name: i18n.translate('xpack.observabilityAiAssistant.chatHeader.actions.settings', { + name: i18n.translate('xpack.aiAssistant.chatHeader.actions.settings', { defaultMessage: 'AI Assistant Settings', }), onClick: () => { @@ -115,7 +112,7 @@ export function ChatActionsMenu({ { name: (
- {i18n.translate('xpack.observabilityAiAssistant.chatHeader.actions.connector', { + {i18n.translate('xpack.aiAssistant.chatHeader.actions.connector', { defaultMessage: 'Connector', })}{' '} @@ -129,12 +126,9 @@ export function ChatActionsMenu({ panel: 1, }, { - name: i18n.translate( - 'xpack.observabilityAiAssistant.chatHeader.actions.copyConversation', - { - defaultMessage: 'Copy conversation', - } - ), + name: i18n.translate('xpack.aiAssistant.chatHeader.actions.copyConversation', { + defaultMessage: 'Copy conversation', + }), disabled: !conversationId, onClick: () => { toggleActionsMenu(); @@ -146,7 +140,7 @@ export function ChatActionsMenu({ { id: 1, width: 256, - title: i18n.translate('xpack.observabilityAiAssistant.chatHeader.actions.connector', { + title: i18n.translate('xpack.aiAssistant.chatHeader.actions.connector', { defaultMessage: 'Connector', }), content: ( @@ -159,10 +153,9 @@ export function ChatActionsMenu({ data-test-subj="settingsTabGoToConnectorsButton" onClick={handleNavigateToConnectors} > - {i18n.translate( - 'xpack.observabilityAiAssistant.settingsPage.goToConnectorsButtonLabel', - { defaultMessage: 'Manage connectors' } - )} + {i18n.translate('xpack.aiAssistant.settingsPage.goToConnectorsButtonLabel', { + defaultMessage: 'Manage connectors', + })} ), diff --git a/x-pack/plugins/observability_solution/observability_ai_assistant_app/public/components/chat/chat_body.stories.tsx b/x-pack/packages/kbn-ai-assistant/src/chat/chat_body.stories.tsx similarity index 98% rename from x-pack/plugins/observability_solution/observability_ai_assistant_app/public/components/chat/chat_body.stories.tsx rename to x-pack/packages/kbn-ai-assistant/src/chat/chat_body.stories.tsx index 4e71ecdfd2c12..182cb046cba70 100644 --- a/x-pack/plugins/observability_solution/observability_ai_assistant_app/public/components/chat/chat_body.stories.tsx +++ b/x-pack/packages/kbn-ai-assistant/src/chat/chat_body.stories.tsx @@ -8,9 +8,9 @@ import { ComponentMeta, ComponentStoryObj } from '@storybook/react'; import React from 'react'; import { MessageRole } from '@kbn/observability-ai-assistant-plugin/public'; -import { KibanaReactStorybookDecorator } from '../../utils/storybook_decorator.stories'; +import { buildSystemMessage } from '../utils/builders'; +import { KibanaReactStorybookDecorator } from '../utils/storybook_decorator.stories'; import { ChatBody as Component } from './chat_body'; -import { buildSystemMessage } from '../../utils/builders'; const meta: ComponentMeta = { component: Component, diff --git a/x-pack/plugins/observability_solution/observability_ai_assistant_app/public/components/chat/chat_body.test.tsx b/x-pack/packages/kbn-ai-assistant/src/chat/chat_body.test.tsx similarity index 100% rename from x-pack/plugins/observability_solution/observability_ai_assistant_app/public/components/chat/chat_body.test.tsx rename to x-pack/packages/kbn-ai-assistant/src/chat/chat_body.test.tsx diff --git a/x-pack/plugins/observability_solution/observability_ai_assistant_app/public/components/chat/chat_body.tsx b/x-pack/packages/kbn-ai-assistant/src/chat/chat_body.tsx similarity index 93% rename from x-pack/plugins/observability_solution/observability_ai_assistant_app/public/components/chat/chat_body.tsx rename to x-pack/packages/kbn-ai-assistant/src/chat/chat_body.tsx index 0bf5a8009b635..c3989f6971fff 100644 --- a/x-pack/plugins/observability_solution/observability_ai_assistant_app/public/components/chat/chat_body.tsx +++ b/x-pack/packages/kbn-ai-assistant/src/chat/chat_body.tsx @@ -31,20 +31,20 @@ import type { AuthenticatedUser } from '@kbn/security-plugin/common'; import { euiThemeVars } from '@kbn/ui-theme'; import { findLastIndex } from 'lodash'; import React, { useCallback, useEffect, useRef, useState } from 'react'; -import { useConversation } from '../../hooks/use_conversation'; -import { useGenAIConnectors } from '../../hooks/use_genai_connectors'; -import type { UseKnowledgeBaseResult } from '../../hooks/use_knowledge_base'; -import { useLicense } from '../../hooks/use_license'; -import { useObservabilityAIAssistantChatService } from '../../hooks/use_observability_ai_assistant_chat_service'; -import { useSimulatedFunctionCalling } from '../../hooks/use_simulated_function_calling'; -import { ASSISTANT_SETUP_TITLE, EMPTY_CONVERSATION_TITLE, UPGRADE_LICENSE_TITLE } from '../../i18n'; -import { PromptEditor } from '../prompt_editor/prompt_editor'; +import type { UseKnowledgeBaseResult } from '../hooks/use_knowledge_base'; +import { ASSISTANT_SETUP_TITLE, EMPTY_CONVERSATION_TITLE, UPGRADE_LICENSE_TITLE } from '../i18n'; +import { useAIAssistantChatService } from '../hooks/use_ai_assistant_chat_service'; +import { useSimulatedFunctionCalling } from '../hooks/use_simulated_function_calling'; +import { useGenAIConnectors } from '../hooks/use_genai_connectors'; +import { useConversation } from '../hooks/use_conversation'; import { FlyoutPositionMode } from './chat_flyout'; import { ChatHeader } from './chat_header'; import { ChatTimeline } from './chat_timeline'; import { IncorrectLicensePanel } from './incorrect_license_panel'; import { SimulatedFunctionCallingCallout } from './simulated_function_calling_callout'; import { WelcomeMessage } from './welcome_message'; +import { useLicense } from '../hooks/use_license'; +import { PromptEditor } from '../prompt_editor/prompt_editor'; const fullHeightClassName = css` height: 100%; @@ -110,7 +110,7 @@ export function ChatBody({ showLinkToConversationsApp, onConversationUpdate, onToggleFlyoutPositionMode, - onClose, + navigateToConversation, }: { connectors: ReturnType; currentUser?: Pick; @@ -122,14 +122,14 @@ export function ChatBody({ showLinkToConversationsApp: boolean; onConversationUpdate: (conversation: { conversation: Conversation['conversation'] }) => void; onToggleFlyoutPositionMode?: (flyoutPositionMode: FlyoutPositionMode) => void; - onClose?: () => void; + navigateToConversation: (conversationId?: string) => void; }) { const license = useLicense(); const hasCorrectLicense = license?.hasAtLeast('enterprise'); const euiTheme = useEuiTheme(); const scrollBarStyles = euiScrollBarStyles(euiTheme); - const chatService = useObservabilityAIAssistantChatService(); + const chatService = useAIAssistantChatService(); const { simulatedFunctionCallingEnabled } = useSimulatedFunctionCalling(); @@ -440,12 +440,12 @@ export function ChatBody({ - {i18n.translate('xpack.observabilityAiAssistant.couldNotFindConversationContent', { + {i18n.translate('xpack.aiAssistant.couldNotFindConversationContent', { defaultMessage: 'Could not find a conversation with id {conversationId}. Make sure the conversation exists and you have access to it.', values: { conversationId: initialConversationId }, @@ -470,12 +470,12 @@ export function ChatBody({ {conversation.error ? ( - {i18n.translate('xpack.observabilityAiAssistant.couldNotFindConversationContent', { + {i18n.translate('xpack.aiAssistant.couldNotFindConversationContent', { defaultMessage: 'Could not find a conversation with id {conversationId}. Make sure the conversation exists and you have access to it.', values: { conversationId: initialConversationId }, @@ -500,7 +500,7 @@ export function ChatBody({ saveTitle(newTitle); }} onToggleFlyoutPositionMode={onToggleFlyoutPositionMode} - onClose={onClose} + navigateToConversation={navigateToConversation} /> diff --git a/x-pack/plugins/observability_solution/observability_ai_assistant_app/public/components/chat/chat_consolidated_items.tsx b/x-pack/packages/kbn-ai-assistant/src/chat/chat_consolidated_items.tsx similarity index 89% rename from x-pack/plugins/observability_solution/observability_ai_assistant_app/public/components/chat/chat_consolidated_items.tsx rename to x-pack/packages/kbn-ai-assistant/src/chat/chat_consolidated_items.tsx index f31796b8812d2..5771b1fd297d7 100644 --- a/x-pack/plugins/observability_solution/observability_ai_assistant_app/public/components/chat/chat_consolidated_items.tsx +++ b/x-pack/packages/kbn-ai-assistant/src/chat/chat_consolidated_items.tsx @@ -90,11 +90,11 @@ export function ChatConsolidatedItems({ > {!expanded - ? i18n.translate('xpack.observabilityAiAssistant.chatCollapsedItems.showEvents', { + ? i18n.translate('xpack.aiAssistant.chatCollapsedItems.showEvents', { defaultMessage: 'Show {count} events', values: { count: consolidatedItem.length }, }) - : i18n.translate('xpack.observabilityAiAssistant.chatCollapsedItems.hideEvents', { + : i18n.translate('xpack.aiAssistant.chatCollapsedItems.hideEvents', { defaultMessage: 'Hide {count} events', values: { count: consolidatedItem.length }, })} @@ -104,12 +104,9 @@ export function ChatConsolidatedItems({ username="" actions={ {}, + navigateToConversation: () => {}, }; export const ChatFlyout = Template.bind({}); diff --git a/x-pack/plugins/observability_solution/observability_ai_assistant_app/public/components/chat/chat_flyout.tsx b/x-pack/packages/kbn-ai-assistant/src/chat/chat_flyout.tsx similarity index 88% rename from x-pack/plugins/observability_solution/observability_ai_assistant_app/public/components/chat/chat_flyout.tsx rename to x-pack/packages/kbn-ai-assistant/src/chat/chat_flyout.tsx index 67ac37a88d724..8d636374ac768 100644 --- a/x-pack/plugins/observability_solution/observability_ai_assistant_app/public/components/chat/chat_flyout.tsx +++ b/x-pack/packages/kbn-ai-assistant/src/chat/chat_flyout.tsx @@ -19,16 +19,16 @@ import { i18n } from '@kbn/i18n'; import { Message } from '@kbn/observability-ai-assistant-plugin/common'; import React, { useState } from 'react'; import ReactDOM from 'react-dom'; -import { useConversationKey } from '../../hooks/use_conversation_key'; -import { useConversationList } from '../../hooks/use_conversation_list'; -import { useCurrentUser } from '../../hooks/use_current_user'; -import { useGenAIConnectors } from '../../hooks/use_genai_connectors'; -import { useKibana } from '../../hooks/use_kibana'; -import { useKnowledgeBase } from '../../hooks/use_knowledge_base'; -import { NewChatButton } from '../buttons/new_chat_button'; +import { useConversationKey } from '../hooks/use_conversation_key'; +import { useConversationList } from '../hooks/use_conversation_list'; +import { useCurrentUser } from '../hooks/use_current_user'; +import { useGenAIConnectors } from '../hooks/use_genai_connectors'; import { ChatBody } from './chat_body'; import { ChatInlineEditingContent } from './chat_inline_edit'; import { ConversationList } from './conversation_list'; +import { useKibana } from '../hooks/use_kibana'; +import { useKnowledgeBase } from '../hooks/use_knowledge_base'; +import { NewChatButton } from '../buttons/new_chat_button'; const CONVERSATIONS_SIDEBAR_WIDTH = 260; const CONVERSATIONS_SIDEBAR_WIDTH_COLLAPSED = 34; @@ -46,12 +46,14 @@ export function ChatFlyout({ initialFlyoutPositionMode, isOpen, onClose, + navigateToConversation, }: { initialTitle: string; initialMessages: Message[]; initialFlyoutPositionMode?: FlyoutPositionMode; isOpen: boolean; onClose: () => void; + navigateToConversation(conversationId?: string): void; }) { const { euiTheme } = useEuiTheme(); const breakpoint = useCurrentEuiBreakpoint(); @@ -75,11 +77,7 @@ export function ChatFlyout({ const { services: { - plugins: { - start: { - observabilityAIAssistant: { ObservabilityAIAssistantMultipaneFlyoutContext }, - }, - }, + observabilityAIAssistant: { ObservabilityAIAssistantMultipaneFlyoutContext }, }, } = useKibana(); const conversationList = useConversationList(); @@ -148,8 +146,8 @@ export function ChatFlyout({ > { + if (onClose) onClose(); + navigateToConversation(newConversationId); + }} /> diff --git a/x-pack/plugins/observability_solution/observability_ai_assistant_app/public/components/chat/chat_header.stories.tsx b/x-pack/packages/kbn-ai-assistant/src/chat/chat_header.stories.tsx similarity index 100% rename from x-pack/plugins/observability_solution/observability_ai_assistant_app/public/components/chat/chat_header.stories.tsx rename to x-pack/packages/kbn-ai-assistant/src/chat/chat_header.stories.tsx diff --git a/x-pack/plugins/observability_solution/observability_ai_assistant_app/public/components/chat/chat_header.tsx b/x-pack/packages/kbn-ai-assistant/src/chat/chat_header.tsx similarity index 81% rename from x-pack/plugins/observability_solution/observability_ai_assistant_app/public/components/chat/chat_header.tsx rename to x-pack/packages/kbn-ai-assistant/src/chat/chat_header.tsx index c67596fbafd5e..c9f0588a1c90f 100644 --- a/x-pack/plugins/observability_solution/observability_ai_assistant_app/public/components/chat/chat_header.tsx +++ b/x-pack/packages/kbn-ai-assistant/src/chat/chat_header.tsx @@ -21,8 +21,7 @@ import { i18n } from '@kbn/i18n'; import { css } from '@emotion/css'; import { AssistantAvatar } from '@kbn/observability-ai-assistant-plugin/public'; import { ChatActionsMenu } from './chat_actions_menu'; -import { useObservabilityAIAssistantRouter } from '../../hooks/use_observability_ai_assistant_router'; -import type { UseGenAIConnectorsResult } from '../../hooks/use_genai_connectors'; +import type { UseGenAIConnectorsResult } from '../hooks/use_genai_connectors'; import { FlyoutPositionMode } from './chat_flyout'; // needed to prevent InlineTextEdit component from expanding container @@ -50,7 +49,7 @@ export function ChatHeader({ onCopyConversation, onSaveTitle, onToggleFlyoutPositionMode, - onClose, + navigateToConversation, }: { connectors: UseGenAIConnectorsResult; conversationId?: string; @@ -61,36 +60,17 @@ export function ChatHeader({ onCopyConversation: () => void; onSaveTitle: (title: string) => void; onToggleFlyoutPositionMode?: (newFlyoutPositionMode: FlyoutPositionMode) => void; - onClose?: () => void; + navigateToConversation: (nextConversationId?: string) => void; }) { const theme = useEuiTheme(); const breakpoint = useCurrentEuiBreakpoint(); - const router = useObservabilityAIAssistantRouter(); - const [newTitle, setNewTitle] = useState(title); useEffect(() => { setNewTitle(title); }, [title]); - const handleNavigateToConversations = () => { - if (onClose) { - onClose(); - } - - if (conversationId) { - router.push('/conversations/{conversationId}', { - path: { - conversationId, - }, - query: {}, - }); - } else { - router.push('/conversations/new', { path: {}, query: {} }); - } - }; - const handleToggleFlyoutPositionMode = () => { if (flyoutPositionMode) { onToggleFlyoutPositionMode?.( @@ -126,10 +106,9 @@ export function ChatHeader({ className={css` color: ${!!title ? theme.euiTheme.colors.text : theme.euiTheme.colors.subduedText}; `} - inputAriaLabel={i18n.translate( - 'xpack.observabilityAiAssistant.chatHeader.editConversationInput', - { defaultMessage: 'Edit conversation' } - )} + inputAriaLabel={i18n.translate('xpack.aiAssistant.chatHeader.editConversationInput', { + defaultMessage: 'Edit conversation', + })} isReadOnly={ !conversationId || !connectors.selectedConnector || @@ -162,11 +141,11 @@ export function ChatHeader({ content={ flyoutPositionMode === 'overlay' ? i18n.translate( - 'xpack.observabilityAiAssistant.chatHeader.euiToolTip.flyoutModeLabel.dock', + 'xpack.aiAssistant.chatHeader.euiToolTip.flyoutModeLabel.dock', { defaultMessage: 'Dock chat' } ) : i18n.translate( - 'xpack.observabilityAiAssistant.chatHeader.euiToolTip.flyoutModeLabel.undock', + 'xpack.aiAssistant.chatHeader.euiToolTip.flyoutModeLabel.undock', { defaultMessage: 'Undock chat' } ) } @@ -174,7 +153,7 @@ export function ChatHeader({ > navigateToConversation(conversationId)} /> } diff --git a/x-pack/plugins/observability_solution/observability_ai_assistant_app/public/components/chat/chat_inline_edit.tsx b/x-pack/packages/kbn-ai-assistant/src/chat/chat_inline_edit.tsx similarity index 100% rename from x-pack/plugins/observability_solution/observability_ai_assistant_app/public/components/chat/chat_inline_edit.tsx rename to x-pack/packages/kbn-ai-assistant/src/chat/chat_inline_edit.tsx diff --git a/x-pack/plugins/observability_solution/observability_ai_assistant_app/public/components/chat/chat_item.tsx b/x-pack/packages/kbn-ai-assistant/src/chat/chat_item.tsx similarity index 98% rename from x-pack/plugins/observability_solution/observability_ai_assistant_app/public/components/chat/chat_item.tsx rename to x-pack/packages/kbn-ai-assistant/src/chat/chat_item.tsx index a1f5d5eb88d2d..23bdbdaea3593 100644 --- a/x-pack/plugins/observability_solution/observability_ai_assistant_app/public/components/chat/chat_item.tsx +++ b/x-pack/packages/kbn-ai-assistant/src/chat/chat_item.tsx @@ -22,11 +22,11 @@ import { Feedback, TelemetryEventTypeWithPayload, } from '@kbn/observability-ai-assistant-plugin/public'; +import { getRoleTranslation } from '../utils/get_role_translation'; import { ChatItemActions } from './chat_item_actions'; import { ChatItemAvatar } from './chat_item_avatar'; import { ChatItemContentInlinePromptEditor } from './chat_item_content_inline_prompt_editor'; import { ChatTimelineItem } from './chat_timeline'; -import { getRoleTranslation } from '../../utils/get_role_translation'; export interface ChatItemProps extends Omit { onActionClick: ChatActionClickHandler; diff --git a/x-pack/plugins/observability_solution/observability_ai_assistant_app/public/components/chat/chat_item_actions.tsx b/x-pack/packages/kbn-ai-assistant/src/chat/chat_item_actions.tsx similarity index 73% rename from x-pack/plugins/observability_solution/observability_ai_assistant_app/public/components/chat/chat_item_actions.tsx rename to x-pack/packages/kbn-ai-assistant/src/chat/chat_item_actions.tsx index 4995b0163b7be..cb1196baa6bc1 100644 --- a/x-pack/plugins/observability_solution/observability_ai_assistant_app/public/components/chat/chat_item_actions.tsx +++ b/x-pack/packages/kbn-ai-assistant/src/chat/chat_item_actions.tsx @@ -46,12 +46,9 @@ export function ChatItemActions({ <> {canEdit ? ( setIsPopoverOpen(undefined)} > - {i18n.translate( - 'xpack.observabilityAiAssistant.chatTimeline.actions.copyMessageSuccessful', - { - defaultMessage: 'Copied message', - } - )} + {i18n.translate('xpack.aiAssistant.chatTimeline.actions.copyMessageSuccessful', { + defaultMessage: 'Copied message', + })} ) : null} diff --git a/x-pack/plugins/observability_solution/observability_ai_assistant_app/public/components/chat/chat_item_avatar.tsx b/x-pack/packages/kbn-ai-assistant/src/chat/chat_item_avatar.tsx similarity index 100% rename from x-pack/plugins/observability_solution/observability_ai_assistant_app/public/components/chat/chat_item_avatar.tsx rename to x-pack/packages/kbn-ai-assistant/src/chat/chat_item_avatar.tsx diff --git a/x-pack/plugins/observability_solution/observability_ai_assistant_app/public/components/chat/chat_item_content_inline_prompt_editor.tsx b/x-pack/packages/kbn-ai-assistant/src/chat/chat_item_content_inline_prompt_editor.tsx similarity index 100% rename from x-pack/plugins/observability_solution/observability_ai_assistant_app/public/components/chat/chat_item_content_inline_prompt_editor.tsx rename to x-pack/packages/kbn-ai-assistant/src/chat/chat_item_content_inline_prompt_editor.tsx diff --git a/x-pack/plugins/observability_solution/observability_ai_assistant_app/public/components/chat/chat_item_title.tsx b/x-pack/packages/kbn-ai-assistant/src/chat/chat_item_title.tsx similarity index 71% rename from x-pack/plugins/observability_solution/observability_ai_assistant_app/public/components/chat/chat_item_title.tsx rename to x-pack/packages/kbn-ai-assistant/src/chat/chat_item_title.tsx index 2749ef3635f40..d6a26b0287e46 100644 --- a/x-pack/plugins/observability_solution/observability_ai_assistant_app/public/components/chat/chat_item_title.tsx +++ b/x-pack/packages/kbn-ai-assistant/src/chat/chat_item_title.tsx @@ -7,6 +7,7 @@ import { euiThemeVars } from '@kbn/ui-theme'; import React, { ReactNode } from 'react'; +import { css } from '@emotion/react'; interface ChatItemTitleProps { actionsTrigger?: ReactNode; @@ -14,14 +15,15 @@ interface ChatItemTitleProps { } export function ChatItemTitle({ actionsTrigger, title }: ChatItemTitleProps) { + const containerCSS = css` + position: absolute; + top: 2; + right: ${euiThemeVars.euiSizeS}; + `; return ( <> {title} - {actionsTrigger ? ( -
- {actionsTrigger} -
- ) : null} + {actionsTrigger ?
{actionsTrigger}
: null} ); } diff --git a/x-pack/plugins/observability_solution/observability_ai_assistant_app/public/components/chat/chat_timeline.stories.tsx b/x-pack/packages/kbn-ai-assistant/src/chat/chat_timeline.stories.tsx similarity index 98% rename from x-pack/plugins/observability_solution/observability_ai_assistant_app/public/components/chat/chat_timeline.stories.tsx rename to x-pack/packages/kbn-ai-assistant/src/chat/chat_timeline.stories.tsx index 88354f41ba293..0afb0c7e79fc0 100644 --- a/x-pack/plugins/observability_solution/observability_ai_assistant_app/public/components/chat/chat_timeline.stories.tsx +++ b/x-pack/packages/kbn-ai-assistant/src/chat/chat_timeline.stories.tsx @@ -18,7 +18,7 @@ import { buildFunctionResponseMessage, buildSystemMessage, buildUserMessage, -} from '../../utils/builders'; +} from '../utils/builders'; import { ChatTimeline as Component, type ChatTimelineProps } from './chat_timeline'; export default { @@ -86,11 +86,11 @@ const defaultProps: ComponentProps = { Mathematical Functions: In mathematics, a function maps input values to corresponding output values based on a specific rule or expression. The general process of how a mathematical function works can be summarized as follows: Step 1: Input - You provide an input value to the function, denoted as 'x' in the notation f(x). This value represents the independent variable. - + Step 2: Processing - The function takes the input value and applies a specific rule or algorithm to it. This rule is defined by the function itself and varies depending on the function's expression. - + Step 3: Output - After processing the input, the function produces an output value, denoted as 'f(x)' or 'y'. This output represents the dependent variable and is the result of applying the function's rule to the input. - + Step 4: Uniqueness - A well-defined mathematical function ensures that each input value corresponds to exactly one output value. In other words, the function should yield the same output for the same input whenever it is called.`, }, }), diff --git a/x-pack/plugins/observability_solution/observability_ai_assistant_app/public/components/chat/chat_timeline.tsx b/x-pack/packages/kbn-ai-assistant/src/chat/chat_timeline.tsx similarity index 96% rename from x-pack/plugins/observability_solution/observability_ai_assistant_app/public/components/chat/chat_timeline.tsx rename to x-pack/packages/kbn-ai-assistant/src/chat/chat_timeline.tsx index ec2cf2ca68e7c..9b349f49f3904 100644 --- a/x-pack/plugins/observability_solution/observability_ai_assistant_app/public/components/chat/chat_timeline.tsx +++ b/x-pack/packages/kbn-ai-assistant/src/chat/chat_timeline.tsx @@ -18,10 +18,10 @@ import { type ObservabilityAIAssistantChatService, type TelemetryEventTypeWithPayload, } from '@kbn/observability-ai-assistant-plugin/public'; -import type { UseKnowledgeBaseResult } from '../../hooks/use_knowledge_base'; +import type { UseKnowledgeBaseResult } from '../hooks/use_knowledge_base'; import { ChatItem } from './chat_item'; import { ChatConsolidatedItems } from './chat_consolidated_items'; -import { getTimelineItemsfromConversation } from '../../utils/get_timeline_items_from_conversation'; +import { getTimelineItemsfromConversation } from '../utils/get_timeline_items_from_conversation'; export interface ChatTimelineItem extends Pick { diff --git a/x-pack/plugins/observability_solution/observability_ai_assistant_app/public/components/chat/conversation_list.stories.tsx b/x-pack/packages/kbn-ai-assistant/src/chat/conversation_list.stories.tsx similarity index 93% rename from x-pack/plugins/observability_solution/observability_ai_assistant_app/public/components/chat/conversation_list.stories.tsx rename to x-pack/packages/kbn-ai-assistant/src/chat/conversation_list.stories.tsx index b0f72e80c5721..7405b477647fd 100644 --- a/x-pack/plugins/observability_solution/observability_ai_assistant_app/public/components/chat/conversation_list.stories.tsx +++ b/x-pack/packages/kbn-ai-assistant/src/chat/conversation_list.stories.tsx @@ -7,8 +7,8 @@ import { ComponentMeta, ComponentStoryObj } from '@storybook/react'; import React from 'react'; -import { buildConversation } from '../../utils/builders'; -import { KibanaReactStorybookDecorator } from '../../utils/storybook_decorator.stories'; +import { buildConversation } from '../utils/builders'; +import { KibanaReactStorybookDecorator } from '../utils/storybook_decorator.stories'; import { ConversationList as Component } from './conversation_list'; type ConversationListProps = React.ComponentProps; diff --git a/x-pack/plugins/observability_solution/observability_ai_assistant_app/public/components/chat/conversation_list.tsx b/x-pack/packages/kbn-ai-assistant/src/chat/conversation_list.tsx similarity index 80% rename from x-pack/plugins/observability_solution/observability_ai_assistant_app/public/components/chat/conversation_list.tsx rename to x-pack/packages/kbn-ai-assistant/src/chat/conversation_list.tsx index 1b26922bcaf69..e4a7022edc763 100644 --- a/x-pack/plugins/observability_solution/observability_ai_assistant_app/public/components/chat/conversation_list.tsx +++ b/x-pack/packages/kbn-ai-assistant/src/chat/conversation_list.tsx @@ -21,10 +21,9 @@ import { import { css } from '@emotion/css'; import { i18n } from '@kbn/i18n'; import React, { MouseEvent } from 'react'; -import { useConfirmModal } from '../../hooks/use_confirm_modal'; -import type { UseConversationListResult } from '../../hooks/use_conversation_list'; -import { useObservabilityAIAssistantRouter } from '../../hooks/use_observability_ai_assistant_router'; -import { EMPTY_CONVERSATION_TITLE } from '../../i18n'; +import { useConfirmModal } from '../hooks/use_confirm_modal'; +import type { UseConversationListResult } from '../hooks/use_conversation_list'; +import { EMPTY_CONVERSATION_TITLE } from '../i18n'; import { NewChatButton } from '../buttons/new_chat_button'; const titleClassName = css` @@ -51,15 +50,17 @@ export function ConversationList({ selectedConversationId, onConversationSelect, onConversationDeleteClick, + newConversationHref, + getConversationHref, }: { conversations: UseConversationListResult['conversations']; isLoading: boolean; selectedConversationId?: string; onConversationSelect?: (conversationId?: string) => void; onConversationDeleteClick: (conversationId: string) => void; + newConversationHref?: string; + getConversationHref?: (conversationId: string) => string; }) { - const router = useObservabilityAIAssistantRouter(); - const euiTheme = useEuiTheme(); const scrollBarStyles = euiScrollBarStyles(euiTheme); @@ -70,21 +71,15 @@ export function ConversationList({ `; const { element: confirmDeleteElement, confirm: confirmDeleteCallback } = useConfirmModal({ - title: i18n.translate('xpack.observabilityAiAssistant.flyout.confirmDeleteConversationTitle', { + title: i18n.translate('xpack.aiAssistant.flyout.confirmDeleteConversationTitle', { defaultMessage: 'Delete this conversation?', }), - children: i18n.translate( - 'xpack.observabilityAiAssistant.flyout.confirmDeleteConversationContent', - { - defaultMessage: 'This action cannot be undone.', - } - ), - confirmButtonText: i18n.translate( - 'xpack.observabilityAiAssistant.flyout.confirmDeleteButtonText', - { - defaultMessage: 'Delete conversation', - } - ), + children: i18n.translate('xpack.aiAssistant.flyout.confirmDeleteConversationContent', { + defaultMessage: 'This action cannot be undone.', + }), + confirmButtonText: i18n.translate('xpack.aiAssistant.flyout.confirmDeleteButtonText', { + defaultMessage: 'Delete conversation', + }), }); const displayedConversations = [ @@ -94,7 +89,7 @@ export function ConversationList({ id: '', label: EMPTY_CONVERSATION_TITLE, lastUpdated: '', - href: router.link('/conversations/new'), + href: newConversationHref, }, ] : []), @@ -102,11 +97,7 @@ export function ConversationList({ id: conversation.id, label: conversation.title, lastUpdated: conversation.last_updated, - href: router.link('/conversations/{conversationId}', { - path: { - conversationId: conversation.id, - }, - }), + href: getConversationHref ? getConversationHref(conversation.id) : undefined, })), ]; @@ -123,7 +114,7 @@ export function ConversationList({ - {i18n.translate('xpack.observabilityAiAssistant.conversationList.title', { + {i18n.translate('xpack.aiAssistant.conversationList.title', { defaultMessage: 'Previously', })} @@ -147,12 +138,9 @@ export function ConversationList({ - {i18n.translate( - 'xpack.observabilityAiAssistant.conversationList.errorMessage', - { - defaultMessage: 'Failed to load', - } - )} + {i18n.translate('xpack.aiAssistant.conversationList.errorMessage', { + defaultMessage: 'Failed to load', + })} @@ -185,7 +173,7 @@ export function ConversationList({ ? { iconType: 'trash', 'aria-label': i18n.translate( - 'xpack.observabilityAiAssistant.conversationList.deleteConversationIconLabel', + 'xpack.aiAssistant.conversationList.deleteConversationIconLabel', { defaultMessage: 'Delete', } @@ -211,12 +199,9 @@ export function ConversationList({ {!isLoading && !conversations.error && !displayedConversations?.length ? ( - {i18n.translate( - 'xpack.observabilityAiAssistant.conversationList.noConversations', - { - defaultMessage: 'No conversations', - } - )} + {i18n.translate('xpack.aiAssistant.conversationList.noConversations', { + defaultMessage: 'No conversations', + })} ) : null} @@ -228,7 +213,7 @@ export function ConversationList({ | MouseEvent ) => { diff --git a/x-pack/plugins/observability_solution/observability_ai_assistant_app/public/components/chat/disclaimer.tsx b/x-pack/packages/kbn-ai-assistant/src/chat/disclaimer.tsx similarity index 91% rename from x-pack/plugins/observability_solution/observability_ai_assistant_app/public/components/chat/disclaimer.tsx rename to x-pack/packages/kbn-ai-assistant/src/chat/disclaimer.tsx index e4eb5176469de..8f9c3abca0e71 100644 --- a/x-pack/plugins/observability_solution/observability_ai_assistant_app/public/components/chat/disclaimer.tsx +++ b/x-pack/packages/kbn-ai-assistant/src/chat/disclaimer.tsx @@ -17,7 +17,7 @@ export function Disclaimer() { textAlign="center" data-test-subj="observabilityAiAssistantDisclaimer" > - {i18n.translate('xpack.observabilityAiAssistant.disclaimer.disclaimerLabel', { + {i18n.translate('xpack.aiAssistant.disclaimer.disclaimerLabel', { defaultMessage: "This chat is powered by an integration with your LLM provider. LLMs are known to sometimes present incorrect information as if it's correct. Elastic supports configuration and connection to the LLM provider and your knowledge base, but is not responsible for the LLM's responses.", })} diff --git a/x-pack/plugins/observability_solution/observability_ai_assistant_app/public/components/chat/function_list_popover.stories.tsx b/x-pack/packages/kbn-ai-assistant/src/chat/function_list_popover.stories.tsx similarity index 91% rename from x-pack/plugins/observability_solution/observability_ai_assistant_app/public/components/chat/function_list_popover.stories.tsx rename to x-pack/packages/kbn-ai-assistant/src/chat/function_list_popover.stories.tsx index a8f1e23b8173d..62da0b2d14ff8 100644 --- a/x-pack/plugins/observability_solution/observability_ai_assistant_app/public/components/chat/function_list_popover.stories.tsx +++ b/x-pack/packages/kbn-ai-assistant/src/chat/function_list_popover.stories.tsx @@ -7,7 +7,7 @@ import { ComponentStory } from '@storybook/react'; import React from 'react'; -import { KibanaReactStorybookDecorator } from '../../utils/storybook_decorator.stories'; +import { KibanaReactStorybookDecorator } from '../utils/storybook_decorator.stories'; import { FunctionListPopover as Component } from './function_list_popover'; export default { diff --git a/x-pack/plugins/observability_solution/observability_ai_assistant_app/public/components/chat/function_list_popover.tsx b/x-pack/packages/kbn-ai-assistant/src/chat/function_list_popover.tsx similarity index 85% rename from x-pack/plugins/observability_solution/observability_ai_assistant_app/public/components/chat/function_list_popover.tsx rename to x-pack/packages/kbn-ai-assistant/src/chat/function_list_popover.tsx index 16df72f48c91b..d24aae12fd8c6 100644 --- a/x-pack/plugins/observability_solution/observability_ai_assistant_app/public/components/chat/function_list_popover.tsx +++ b/x-pack/packages/kbn-ai-assistant/src/chat/function_list_popover.tsx @@ -22,7 +22,7 @@ import type { EuiSelectableOptionCheckedType } from '@elastic/eui/src/components import { i18n } from '@kbn/i18n'; import { FunctionVisibility } from '@kbn/observability-ai-assistant-plugin/public'; import type { FunctionDefinition } from '@kbn/observability-ai-assistant-plugin/common'; -import { useObservabilityAIAssistantChatService } from '../../hooks/use_observability_ai_assistant_chat_service'; +import { useAIAssistantChatService } from '../hooks/use_ai_assistant_chat_service'; interface FunctionListOption { label: string; @@ -40,7 +40,7 @@ export function FunctionListPopover({ onSelectFunction: (func: string | undefined) => void; disabled: boolean; }) { - const { getFunctions } = useObservabilityAIAssistantChatService(); + const { getFunctions } = useAIAssistantChatService(); const functions = getFunctions(); const [functionOptions, setFunctionOptions] = useState< @@ -80,21 +80,18 @@ export function FunctionListPopover({ content={ mode === 'prompt' ? i18n.translate( - 'xpack.observabilityAiAssistant.functionListPopover.euiToolTip.selectAFunctionLabel', + 'xpack.aiAssistant.functionListPopover.euiToolTip.selectAFunctionLabel', { defaultMessage: 'Select a function' } ) - : i18n.translate( - 'xpack.observabilityAiAssistant.functionListPopover.euiToolTip.clearFunction', - { - defaultMessage: 'Clear function', - } - ) + : i18n.translate('xpack.aiAssistant.functionListPopover.euiToolTip.clearFunction', { + defaultMessage: 'Clear function', + }) } display="block" > - +

{UPGRADE_LICENSE_TITLE}

- {i18n.translate('xpack.observabilityAiAssistant.incorrectLicense.body', { + {i18n.translate('xpack.aiAssistant.incorrectLicense.body', { defaultMessage: 'You need an Enterprise license to use the Elastic AI Assistant.', })} @@ -57,12 +57,9 @@ export function IncorrectLicensePanel() { href="https://www.elastic.co/subscriptions" target="_blank" > - {i18n.translate( - 'xpack.observabilityAiAssistant.incorrectLicense.subscriptionPlansButton', - { - defaultMessage: 'Subscription plans', - } - )} + {i18n.translate('xpack.aiAssistant.incorrectLicense.subscriptionPlansButton', { + defaultMessage: 'Subscription plans', + })}
@@ -70,7 +67,7 @@ export function IncorrectLicensePanel() { data-test-subj="observabilityAiAssistantIncorrectLicensePanelManageLicenseButton" onClick={handleNavigateToLicenseManagement} > - {i18n.translate('xpack.observabilityAiAssistant.incorrectLicense.manageLicense', { + {i18n.translate('xpack.aiAssistant.incorrectLicense.manageLicense', { defaultMessage: 'Manage license', })} diff --git a/x-pack/packages/kbn-ai-assistant/src/chat/index.ts b/x-pack/packages/kbn-ai-assistant/src/chat/index.ts new file mode 100644 index 0000000000000..4b04d7dec81c1 --- /dev/null +++ b/x-pack/packages/kbn-ai-assistant/src/chat/index.ts @@ -0,0 +1,11 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +export * from './chat_body'; +export * from './chat_inline_edit'; +export * from './conversation_list'; +export * from './chat_flyout'; diff --git a/x-pack/plugins/observability_solution/observability_ai_assistant_app/public/components/chat/knowledge_base_callout.stories.tsx b/x-pack/packages/kbn-ai-assistant/src/chat/knowledge_base_callout.stories.tsx similarity index 95% rename from x-pack/plugins/observability_solution/observability_ai_assistant_app/public/components/chat/knowledge_base_callout.stories.tsx rename to x-pack/packages/kbn-ai-assistant/src/chat/knowledge_base_callout.stories.tsx index d66729dc75a3d..e87aa161d80c3 100644 --- a/x-pack/plugins/observability_solution/observability_ai_assistant_app/public/components/chat/knowledge_base_callout.stories.tsx +++ b/x-pack/packages/kbn-ai-assistant/src/chat/knowledge_base_callout.stories.tsx @@ -7,7 +7,7 @@ import { ComponentMeta, ComponentStoryObj } from '@storybook/react'; import { merge } from 'lodash'; -import { KibanaReactStorybookDecorator } from '../../utils/storybook_decorator.stories'; +import { KibanaReactStorybookDecorator } from '../utils/storybook_decorator.stories'; import { KnowledgeBaseCallout as Component } from './knowledge_base_callout'; const meta: ComponentMeta = { diff --git a/x-pack/plugins/observability_solution/observability_ai_assistant_app/public/components/chat/knowledge_base_callout.tsx b/x-pack/packages/kbn-ai-assistant/src/chat/knowledge_base_callout.tsx similarity index 85% rename from x-pack/plugins/observability_solution/observability_ai_assistant_app/public/components/chat/knowledge_base_callout.tsx rename to x-pack/packages/kbn-ai-assistant/src/chat/knowledge_base_callout.tsx index 36d6842286aa8..abb296713b2d2 100644 --- a/x-pack/plugins/observability_solution/observability_ai_assistant_app/public/components/chat/knowledge_base_callout.tsx +++ b/x-pack/packages/kbn-ai-assistant/src/chat/knowledge_base_callout.tsx @@ -17,7 +17,7 @@ import { EuiText, } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; -import { UseKnowledgeBaseResult } from '../../hooks/use_knowledge_base'; +import { UseKnowledgeBaseResult } from '../hooks/use_knowledge_base'; export function KnowledgeBaseCallout({ knowledgeBase }: { knowledgeBase: UseKnowledgeBaseResult }) { let content: React.ReactNode; @@ -32,7 +32,7 @@ export function KnowledgeBaseCallout({ knowledgeBase }: { knowledgeBase: UseKnow - {i18n.translate('xpack.observabilityAiAssistant.checkingKbAvailability', { + {i18n.translate('xpack.aiAssistant.checkingKbAvailability', { defaultMessage: 'Checking availability of knowledge base', })} @@ -43,7 +43,7 @@ export function KnowledgeBaseCallout({ knowledgeBase }: { knowledgeBase: UseKnow color = 'danger'; content = ( - {i18n.translate('xpack.observabilityAiAssistant.failedToGetStatus', { + {i18n.translate('xpack.aiAssistant.failedToGetStatus', { defaultMessage: 'Failed to get model status.', })} @@ -53,7 +53,7 @@ export function KnowledgeBaseCallout({ knowledgeBase }: { knowledgeBase: UseKnow content = ( {' '} - {i18n.translate('xpack.observabilityAiAssistant.poweredByModel', { + {i18n.translate('xpack.aiAssistant.poweredByModel', { defaultMessage: 'Powered by {model}', values: { model: 'ELSER', @@ -70,7 +70,7 @@ export function KnowledgeBaseCallout({ knowledgeBase }: { knowledgeBase: UseKnow - {i18n.translate('xpack.observabilityAiAssistant.installingKb', { + {i18n.translate('xpack.aiAssistant.installingKb', { defaultMessage: 'Setting up the knowledge base', })} @@ -81,7 +81,7 @@ export function KnowledgeBaseCallout({ knowledgeBase }: { knowledgeBase: UseKnow color = 'danger'; content = ( - {i18n.translate('xpack.observabilityAiAssistant.failedToSetupKnowledgeBase', { + {i18n.translate('xpack.aiAssistant.failedToSetupKnowledgeBase', { defaultMessage: 'Failed to set up knowledge base.', })} @@ -96,7 +96,7 @@ export function KnowledgeBaseCallout({ knowledgeBase }: { knowledgeBase: UseKnow > {' '} - {i18n.translate('xpack.observabilityAiAssistant.setupKb', { + {i18n.translate('xpack.aiAssistant.setupKb', { defaultMessage: 'Improve your experience by setting up the knowledge base.', })} diff --git a/x-pack/plugins/observability_solution/observability_ai_assistant_app/public/components/chat/simulated_function_calling_callout.tsx b/x-pack/packages/kbn-ai-assistant/src/chat/simulated_function_calling_callout.tsx similarity index 90% rename from x-pack/plugins/observability_solution/observability_ai_assistant_app/public/components/chat/simulated_function_calling_callout.tsx rename to x-pack/packages/kbn-ai-assistant/src/chat/simulated_function_calling_callout.tsx index 41b14e683dd64..26eb589b25dfc 100644 --- a/x-pack/plugins/observability_solution/observability_ai_assistant_app/public/components/chat/simulated_function_calling_callout.tsx +++ b/x-pack/packages/kbn-ai-assistant/src/chat/simulated_function_calling_callout.tsx @@ -17,7 +17,7 @@ export function SimulatedFunctionCallingCallout() { - {i18n.translate('xpack.observabilityAiAssistant.simulatedFunctionCallingCalloutLabel', { + {i18n.translate('xpack.aiAssistant.simulatedFunctionCallingCalloutLabel', { defaultMessage: 'Simulated function calling is enabled. You might see degradated performance.', })} diff --git a/x-pack/plugins/observability_solution/observability_ai_assistant_app/public/components/chat/starter_prompts.tsx b/x-pack/packages/kbn-ai-assistant/src/chat/starter_prompts.tsx similarity index 85% rename from x-pack/plugins/observability_solution/observability_ai_assistant_app/public/components/chat/starter_prompts.tsx rename to x-pack/packages/kbn-ai-assistant/src/chat/starter_prompts.tsx index 1f5402978d41d..faaecc0024135 100644 --- a/x-pack/plugins/observability_solution/observability_ai_assistant_app/public/components/chat/starter_prompts.tsx +++ b/x-pack/packages/kbn-ai-assistant/src/chat/starter_prompts.tsx @@ -16,9 +16,9 @@ import { } from '@elastic/eui'; import { css } from '@emotion/css'; import { uniq } from 'lodash'; -import { useObservabilityAIAssistantAppService } from '../../hooks/use_observability_ai_assistant_app_service'; -import { useGenAIConnectors } from '../../hooks/use_genai_connectors'; -import { nonNullable } from '../../utils/non_nullable'; +import { useAIAssistantAppService } from '../hooks/use_ai_assistant_app_service'; +import { useGenAIConnectors } from '../hooks/use_genai_connectors'; +import { nonNullable } from '../utils/non_nullable'; const starterPromptClassName = css` max-width: 50%; @@ -30,7 +30,7 @@ const starterPromptInnerClassName = css` `; export function StarterPrompts({ onSelectPrompt }: { onSelectPrompt: (prompt: string) => void }) { - const service = useObservabilityAIAssistantAppService(); + const service = useAIAssistantAppService(); const { connectors } = useGenAIConnectors(); diff --git a/x-pack/plugins/observability_solution/observability_ai_assistant_app/public/components/chat/welcome_message.tsx b/x-pack/packages/kbn-ai-assistant/src/chat/welcome_message.tsx similarity index 83% rename from x-pack/plugins/observability_solution/observability_ai_assistant_app/public/components/chat/welcome_message.tsx rename to x-pack/packages/kbn-ai-assistant/src/chat/welcome_message.tsx index 18f4c5598c6fd..a449235ba44e6 100644 --- a/x-pack/plugins/observability_solution/observability_ai_assistant_app/public/components/chat/welcome_message.tsx +++ b/x-pack/packages/kbn-ai-assistant/src/chat/welcome_message.tsx @@ -5,19 +5,19 @@ * 2.0. */ -import React, { useState } from 'react'; +import React, { useMemo, useState } from 'react'; import { css } from '@emotion/css'; import { EuiFlexGroup, EuiFlexItem, EuiSpacer, useCurrentEuiBreakpoint } from '@elastic/eui'; import type { ActionConnector } from '@kbn/triggers-actions-ui-plugin/public'; import { GenerativeAIForObservabilityConnectorFeatureId } from '@kbn/actions-plugin/common'; import { isSupportedConnectorType } from '@kbn/observability-ai-assistant-plugin/public'; -import type { UseKnowledgeBaseResult } from '../../hooks/use_knowledge_base'; -import type { UseGenAIConnectorsResult } from '../../hooks/use_genai_connectors'; +import type { UseKnowledgeBaseResult } from '../hooks/use_knowledge_base'; +import type { UseGenAIConnectorsResult } from '../hooks/use_genai_connectors'; import { Disclaimer } from './disclaimer'; import { WelcomeMessageConnectors } from './welcome_message_connectors'; import { WelcomeMessageKnowledgeBase } from './welcome_message_knowledge_base'; -import { useKibana } from '../../hooks/use_kibana'; import { StarterPrompts } from './starter_prompts'; +import { useKibana } from '../hooks/use_kibana'; const fullHeightClassName = css` height: 100%; @@ -39,22 +39,15 @@ export function WelcomeMessage({ }) { const breakpoint = useCurrentEuiBreakpoint(); - const { - application: { navigateToApp, capabilities }, - plugins: { - start: { - triggersActionsUi: { getAddConnectorFlyout: ConnectorFlyout }, - }, - }, - } = useKibana().services; + const { application, triggersActionsUi } = useKibana().services; const [connectorFlyoutOpen, setConnectorFlyoutOpen] = useState(false); const handleConnectorClick = () => { - if (capabilities.management?.insightsAndAlerting?.triggersActions) { + if (application?.capabilities.management?.insightsAndAlerting?.triggersActions) { setConnectorFlyoutOpen(true); } else { - navigateToApp('management', { + application?.navigateToApp('management', { path: '/insightsAndAlerting/triggersActionsConnectors/connectors', }); } @@ -72,6 +65,11 @@ export function WelcomeMessage({ } }; + const ConnectorFlyout = useMemo( + () => triggersActionsUi.getAddConnectorFlyout, + [triggersActionsUi] + ); + return ( <> {isForbiddenError ? i18n.translate( - 'xpack.observabilityAiAssistant.welcomeMessageConnectors.connectorsForbiddenTextLabel', + 'xpack.aiAssistant.welcomeMessageConnectors.connectorsForbiddenTextLabel', { defaultMessage: 'Required privileges to get connectors are missing' } ) : i18n.translate( - 'xpack.observabilityAiAssistant.welcomeMessageConnectors.connectorsErrorTextLabel', + 'xpack.aiAssistant.welcomeMessageConnectors.connectorsErrorTextLabel', { defaultMessage: 'Could not load connectors' } )} @@ -72,21 +72,15 @@ export function WelcomeMessageConnectors({ return !connectors.loading && connectors.connectors?.length === 0 && onSetupConnectorClick ? (
- {i18n.translate( - 'xpack.observabilityAiAssistant.initialSetupPanel.setupConnector.description2', - { - defaultMessage: - 'Start working with the Elastic AI Assistant by setting up a connector for your AI provider. The model needs to support function calls. When using OpenAI or Azure, we recommend using GPT4.', - } - )} + {i18n.translate('xpack.aiAssistant.initialSetupPanel.setupConnector.description2', { + defaultMessage: + 'Start working with the Elastic AI Assistant by setting up a connector for your AI provider. The model needs to support function calls. When using OpenAI or Azure, we recommend using GPT4.', + })} - {i18n.translate( - 'xpack.observabilityAiAssistant.initialSetupPanel.setupConnector.buttonLabel', - { - defaultMessage: 'Set up GenAI connector', - } - )} + {i18n.translate('xpack.aiAssistant.initialSetupPanel.setupConnector.buttonLabel', { + defaultMessage: 'Set up GenAI connector', + })}
diff --git a/x-pack/plugins/observability_solution/observability_ai_assistant_app/public/components/chat/welcome_message_knowledge_base.tsx b/x-pack/packages/kbn-ai-assistant/src/chat/welcome_message_knowledge_base.tsx similarity index 81% rename from x-pack/plugins/observability_solution/observability_ai_assistant_app/public/components/chat/welcome_message_knowledge_base.tsx rename to x-pack/packages/kbn-ai-assistant/src/chat/welcome_message_knowledge_base.tsx index afdbed9ed4c43..72653473c41ae 100644 --- a/x-pack/plugins/observability_solution/observability_ai_assistant_app/public/components/chat/welcome_message_knowledge_base.tsx +++ b/x-pack/packages/kbn-ai-assistant/src/chat/welcome_message_knowledge_base.tsx @@ -22,8 +22,8 @@ import usePrevious from 'react-use/lib/usePrevious'; import useTimeoutFn from 'react-use/lib/useTimeoutFn'; import useInterval from 'react-use/lib/useInterval'; import { WelcomeMessageKnowledgeBaseSetupErrorPanel } from './welcome_message_knowledge_base_setup_error_panel'; -import type { UseKnowledgeBaseResult } from '../../hooks/use_knowledge_base'; -import type { UseGenAIConnectorsResult } from '../../hooks/use_genai_connectors'; +import type { UseKnowledgeBaseResult } from '../hooks/use_knowledge_base'; +import type { UseGenAIConnectorsResult } from '../hooks/use_genai_connectors'; export function WelcomeMessageKnowledgeBase({ connectors, @@ -80,13 +80,10 @@ export function WelcomeMessageKnowledgeBase({ {knowledgeBase.isInstalling ? ( <> - {i18n.translate( - 'xpack.observabilityAiAssistant.welcomeMessage.weAreSettingUpTextLabel', - { - defaultMessage: - 'We are setting up your knowledge base. This may take a few minutes. You can continue to use the Assistant while this process is underway.', - } - )} + {i18n.translate('xpack.aiAssistant.welcomeMessage.weAreSettingUpTextLabel', { + defaultMessage: + 'We are setting up your knowledge base. This may take a few minutes. You can continue to use the Assistant while this process is underway.', + })} @@ -96,10 +93,9 @@ export function WelcomeMessageKnowledgeBase({ isLoading onClick={noop} > - {i18n.translate( - 'xpack.observabilityAiAssistant.welcomeMessage.div.settingUpKnowledgeBaseLabel', - { defaultMessage: 'Setting up Knowledge base' } - )} + {i18n.translate('xpack.aiAssistant.welcomeMessage.div.settingUpKnowledgeBaseLabel', { + defaultMessage: 'Setting up Knowledge base', + })} ) : null} @@ -112,7 +108,7 @@ export function WelcomeMessageKnowledgeBase({ <> {i18n.translate( - 'xpack.observabilityAiAssistant.welcomeMessageKnowledgeBase.yourKnowledgeBaseIsNotSetUpCorrectlyLabel', + 'xpack.aiAssistant.welcomeMessageKnowledgeBase.yourKnowledgeBaseIsNotSetUpCorrectlyLabel', { defaultMessage: `Your Knowledge base hasn't been set up.` } )} @@ -130,12 +126,9 @@ export function WelcomeMessageKnowledgeBase({ iconType="importAction" onClick={handleRetryInstall} > - {i18n.translate( - 'xpack.observabilityAiAssistant.welcomeMessage.retryButtonLabel', - { - defaultMessage: 'Install Knowledge base', - } - )} + {i18n.translate('xpack.aiAssistant.welcomeMessage.retryButtonLabel', { + defaultMessage: 'Install Knowledge base', + })}
@@ -149,7 +142,7 @@ export function WelcomeMessageKnowledgeBase({ onClick={() => setIsPopoverOpen(!isPopoverOpen)} > {i18n.translate( - 'xpack.observabilityAiAssistant.welcomeMessage.inspectErrorsButtonEmptyLabel', + 'xpack.aiAssistant.welcomeMessage.inspectErrorsButtonEmptyLabel', { defaultMessage: 'Inspect issues' } )} @@ -180,7 +173,7 @@ export function WelcomeMessageKnowledgeBase({ {i18n.translate( - 'xpack.observabilityAiAssistant.welcomeMessage.knowledgeBaseSuccessfullyInstalledLabel', + 'xpack.aiAssistant.welcomeMessage.knowledgeBaseSuccessfullyInstalledLabel', { defaultMessage: 'Knowledge base successfully installed' } )} diff --git a/x-pack/plugins/observability_solution/observability_ai_assistant_app/public/components/chat/welcome_message_knowledge_base_setup_error_panel.tsx b/x-pack/packages/kbn-ai-assistant/src/chat/welcome_message_knowledge_base_setup_error_panel.tsx similarity index 80% rename from x-pack/plugins/observability_solution/observability_ai_assistant_app/public/components/chat/welcome_message_knowledge_base_setup_error_panel.tsx rename to x-pack/packages/kbn-ai-assistant/src/chat/welcome_message_knowledge_base_setup_error_panel.tsx index a9a6fcff85240..eeff9c8afd7f3 100644 --- a/x-pack/plugins/observability_solution/observability_ai_assistant_app/public/components/chat/welcome_message_knowledge_base_setup_error_panel.tsx +++ b/x-pack/packages/kbn-ai-assistant/src/chat/welcome_message_knowledge_base_setup_error_panel.tsx @@ -21,8 +21,8 @@ import { EuiPanel, } from '@elastic/eui'; import { css } from '@emotion/css'; -import { useKibana } from '../../hooks/use_kibana'; -import type { UseKnowledgeBaseResult } from '../../hooks/use_knowledge_base'; +import { useKibana } from '../hooks/use_kibana'; +import type { UseKnowledgeBaseResult } from '../hooks/use_knowledge_base'; const panelContainerClassName = css` width: 330px; @@ -47,10 +47,9 @@ export function WelcomeMessageKnowledgeBaseSetupErrorPanel({ - {i18n.translate( - 'xpack.observabilityAiAssistant.welcomeMessage.issuesDescriptionListTitleLabel', - { defaultMessage: 'Issues' } - )} + {i18n.translate('xpack.aiAssistant.welcomeMessage.issuesDescriptionListTitleLabel', { + defaultMessage: 'Issues', + })} @@ -61,7 +60,7 @@ export function WelcomeMessageKnowledgeBaseSetupErrorPanel({
  • {' '} {modelName}, @@ -75,7 +74,7 @@ export function WelcomeMessageKnowledgeBaseSetupErrorPanel({
  • {' '} {modelName}, @@ -92,7 +91,7 @@ export function WelcomeMessageKnowledgeBaseSetupErrorPanel({
  • {' '} {modelName}, @@ -113,7 +112,7 @@ export function WelcomeMessageKnowledgeBaseSetupErrorPanel({ {i18n.translate( - 'xpack.observabilityAiAssistant.welcomeMessageKnowledgeBaseSetupErrorPanel.retryInstallingLinkLabel', + 'xpack.aiAssistant.welcomeMessageKnowledgeBaseSetupErrorPanel.retryInstallingLinkLabel', { defaultMessage: 'Retry install' } )} @@ -133,13 +132,12 @@ export function WelcomeMessageKnowledgeBaseSetupErrorPanel({ - {i18n.translate( - 'xpack.observabilityAiAssistant.welcomeMessage.trainedModelsLinkLabel', - { defaultMessage: 'Trained Models' } - )} + {i18n.translate('xpack.aiAssistant.welcomeMessage.trainedModelsLinkLabel', { + defaultMessage: 'Trained Models', + })} ), }} diff --git a/x-pack/plugins/observability_solution/observability_ai_assistant_app/public/routes/conversations/conversation_view.tsx b/x-pack/packages/kbn-ai-assistant/src/conversation/conversation_view.tsx similarity index 71% rename from x-pack/plugins/observability_solution/observability_ai_assistant_app/public/routes/conversations/conversation_view.tsx rename to x-pack/packages/kbn-ai-assistant/src/conversation/conversation_view.tsx index da34c98b86fbc..260a7cb5c10ed 100644 --- a/x-pack/plugins/observability_solution/observability_ai_assistant_app/public/routes/conversations/conversation_view.tsx +++ b/x-pack/packages/kbn-ai-assistant/src/conversation/conversation_view.tsx @@ -9,44 +9,44 @@ import { css } from '@emotion/css'; import { euiThemeVars } from '@kbn/ui-theme'; import React, { useEffect, useState } from 'react'; import ReactDOM from 'react-dom'; -import { useAbortableAsync } from '@kbn/observability-ai-assistant-plugin/public'; -import { ChatBody } from '../../components/chat/chat_body'; -import { ChatInlineEditingContent } from '../../components/chat/chat_inline_edit'; -import { ConversationList } from '../../components/chat/conversation_list'; -import { useCurrentUser } from '../../hooks/use_current_user'; -import { useGenAIConnectors } from '../../hooks/use_genai_connectors'; -import { useKnowledgeBase } from '../../hooks/use_knowledge_base'; -import { useObservabilityAIAssistantParams } from '../../hooks/use_observability_ai_assistant_params'; -import { useObservabilityAIAssistantRouter } from '../../hooks/use_observability_ai_assistant_router'; -import { useObservabilityAIAssistantAppService } from '../../hooks/use_observability_ai_assistant_app_service'; -import { useKibana } from '../../hooks/use_kibana'; -import { useConversationKey } from '../../hooks/use_conversation_key'; -import { useConversationList } from '../../hooks/use_conversation_list'; +import { useKibana } from '../hooks/use_kibana'; +import { ConversationList, ChatBody, ChatInlineEditingContent } from '../chat'; +import { useConversationKey } from '../hooks/use_conversation_key'; +import { useCurrentUser } from '../hooks/use_current_user'; +import { useGenAIConnectors } from '../hooks/use_genai_connectors'; +import { useKnowledgeBase } from '../hooks/use_knowledge_base'; +import { useAIAssistantAppService } from '../hooks/use_ai_assistant_app_service'; +import { useAbortableAsync } from '../hooks/use_abortable_async'; +import { useConversationList } from '../hooks/use_conversation_list'; const SECOND_SLOT_CONTAINER_WIDTH = 400; -export function ConversationView() { +interface ConversationViewProps { + conversationId?: string; + navigateToConversation: (nextConversationId?: string) => void; + getConversationHref?: (conversationId: string) => string; + newConversationHref?: string; +} + +export const ConversationView: React.FC = ({ + conversationId, + navigateToConversation, + getConversationHref, + newConversationHref, +}) => { const { euiTheme } = useEuiTheme(); const currentUser = useCurrentUser(); - const service = useObservabilityAIAssistantAppService(); + const service = useAIAssistantAppService(); const connectors = useGenAIConnectors(); const knowledgeBase = useKnowledgeBase(); - const observabilityAIAssistantRouter = useObservabilityAIAssistantRouter(); - - const { path } = useObservabilityAIAssistantParams('/conversations/*'); - const { services: { - plugins: { - start: { - observabilityAIAssistant: { ObservabilityAIAssistantChatServiceContext }, - }, - }, + observabilityAIAssistant: { ObservabilityAIAssistantChatServiceContext }, }, } = useKibana(); @@ -57,8 +57,6 @@ export function ConversationView() { [service] ); - const conversationId = 'conversationId' in path ? path.conversationId : undefined; - const { key: bodyKey, updateConversationIdInPlace } = useConversationKey(conversationId); const [secondSlotContainer, setSecondSlotContainer] = useState(null); @@ -66,19 +64,6 @@ export function ConversationView() { const conversationList = useConversationList(); - function navigateToConversation(nextConversationId?: string) { - if (nextConversationId) { - observabilityAIAssistantRouter.push('/conversations/{conversationId}', { - path: { - conversationId: nextConversationId, - }, - query: {}, - }); - } else { - observabilityAIAssistantRouter.push('/conversations/new', { path: {}, query: {} }); - } - } - function handleRefreshConversations() { conversationList.conversations.refresh(); } @@ -153,6 +138,9 @@ export function ConversationView() { } }); }} + newConversationHref={newConversationHref} + onConversationSelect={navigateToConversation} + getConversationHref={getConversationHref} /> @@ -176,6 +164,7 @@ export function ConversationView() { knowledgeBase={knowledgeBase} showLinkToConversationsApp={false} onConversationUpdate={handleConversationUpdate} + navigateToConversation={navigateToConversation} />
    @@ -189,4 +178,4 @@ export function ConversationView() { )} ); -} +}; diff --git a/x-pack/plugins/observability_solution/observability_ai_assistant_app/public/hooks/__storybook_mocks__/use_chat.ts b/x-pack/packages/kbn-ai-assistant/src/hooks/__storybook_mocks__/use_chat.ts similarity index 100% rename from x-pack/plugins/observability_solution/observability_ai_assistant_app/public/hooks/__storybook_mocks__/use_chat.ts rename to x-pack/packages/kbn-ai-assistant/src/hooks/__storybook_mocks__/use_chat.ts diff --git a/x-pack/plugins/observability_solution/observability_ai_assistant_app/public/hooks/__storybook_mocks__/use_conversation.ts b/x-pack/packages/kbn-ai-assistant/src/hooks/__storybook_mocks__/use_conversation.ts similarity index 100% rename from x-pack/plugins/observability_solution/observability_ai_assistant_app/public/hooks/__storybook_mocks__/use_conversation.ts rename to x-pack/packages/kbn-ai-assistant/src/hooks/__storybook_mocks__/use_conversation.ts diff --git a/x-pack/plugins/observability_solution/observability_ai_assistant_app/public/hooks/__storybook_mocks__/use_conversation_list.ts b/x-pack/packages/kbn-ai-assistant/src/hooks/__storybook_mocks__/use_conversation_list.ts similarity index 100% rename from x-pack/plugins/observability_solution/observability_ai_assistant_app/public/hooks/__storybook_mocks__/use_conversation_list.ts rename to x-pack/packages/kbn-ai-assistant/src/hooks/__storybook_mocks__/use_conversation_list.ts diff --git a/x-pack/plugins/observability_solution/observability_ai_assistant_app/public/hooks/__storybook_mocks__/use_conversations.ts b/x-pack/packages/kbn-ai-assistant/src/hooks/__storybook_mocks__/use_conversations.ts similarity index 100% rename from x-pack/plugins/observability_solution/observability_ai_assistant_app/public/hooks/__storybook_mocks__/use_conversations.ts rename to x-pack/packages/kbn-ai-assistant/src/hooks/__storybook_mocks__/use_conversations.ts diff --git a/x-pack/plugins/observability_solution/observability_ai_assistant_app/public/hooks/__storybook_mocks__/use_current_user.ts b/x-pack/packages/kbn-ai-assistant/src/hooks/__storybook_mocks__/use_current_user.ts similarity index 100% rename from x-pack/plugins/observability_solution/observability_ai_assistant_app/public/hooks/__storybook_mocks__/use_current_user.ts rename to x-pack/packages/kbn-ai-assistant/src/hooks/__storybook_mocks__/use_current_user.ts diff --git a/x-pack/plugins/observability_solution/observability_ai_assistant_app/public/hooks/__storybook_mocks__/use_genai_connectors.ts b/x-pack/packages/kbn-ai-assistant/src/hooks/__storybook_mocks__/use_genai_connectors.ts similarity index 100% rename from x-pack/plugins/observability_solution/observability_ai_assistant_app/public/hooks/__storybook_mocks__/use_genai_connectors.ts rename to x-pack/packages/kbn-ai-assistant/src/hooks/__storybook_mocks__/use_genai_connectors.ts diff --git a/x-pack/plugins/observability_solution/observability_ai_assistant_app/public/hooks/__storybook_mocks__/use_knowledge_base.ts b/x-pack/packages/kbn-ai-assistant/src/hooks/__storybook_mocks__/use_knowledge_base.ts similarity index 100% rename from x-pack/plugins/observability_solution/observability_ai_assistant_app/public/hooks/__storybook_mocks__/use_knowledge_base.ts rename to x-pack/packages/kbn-ai-assistant/src/hooks/__storybook_mocks__/use_knowledge_base.ts diff --git a/x-pack/packages/kbn-ai-assistant/src/hooks/index.ts b/x-pack/packages/kbn-ai-assistant/src/hooks/index.ts new file mode 100644 index 0000000000000..ee630d1caec82 --- /dev/null +++ b/x-pack/packages/kbn-ai-assistant/src/hooks/index.ts @@ -0,0 +1,10 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +export * from './use_ai_assistant_app_service'; +export * from './use_ai_assistant_chat_service'; +export * from './use_knowledge_base'; diff --git a/x-pack/packages/kbn-ai-assistant/src/hooks/use_abortable_async.ts b/x-pack/packages/kbn-ai-assistant/src/hooks/use_abortable_async.ts new file mode 100644 index 0000000000000..433ca877b0f62 --- /dev/null +++ b/x-pack/packages/kbn-ai-assistant/src/hooks/use_abortable_async.ts @@ -0,0 +1,87 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ +import { isPromise } from '@kbn/std'; +import { useEffect, useMemo, useRef, useState } from 'react'; + +interface State { + error?: Error; + value?: T; + loading: boolean; +} + +export type AbortableAsyncState = (T extends Promise + ? State + : State) & { refresh: () => void }; + +export function useAbortableAsync( + fn: ({}: { signal: AbortSignal }) => T | Promise, + deps: any[], + options?: { clearValueOnNext?: boolean; defaultValue?: () => T } +): AbortableAsyncState { + const clearValueOnNext = options?.clearValueOnNext; + + const controllerRef = useRef(new AbortController()); + + const [refreshId, setRefreshId] = useState(0); + + const [error, setError] = useState(); + const [loading, setLoading] = useState(false); + const [value, setValue] = useState(options?.defaultValue); + + useEffect(() => { + controllerRef.current.abort(); + + const controller = new AbortController(); + controllerRef.current = controller; + + if (clearValueOnNext) { + setValue(undefined); + setError(undefined); + } + + try { + const response = fn({ signal: controller.signal }); + if (isPromise(response)) { + setLoading(true); + response + .then((nextValue) => { + setError(undefined); + setValue(nextValue); + }) + .catch((err) => { + setValue(undefined); + setError(err); + }) + .finally(() => setLoading(false)); + } else { + setError(undefined); + setValue(response); + setLoading(false); + } + } catch (err) { + setValue(undefined); + setError(err); + setLoading(false); + } + + return () => { + controller.abort(); + }; + // eslint-disable-next-line react-hooks/exhaustive-deps + }, deps.concat(refreshId, clearValueOnNext)); + + return useMemo>(() => { + return { + error, + loading, + value, + refresh: () => { + setRefreshId((id) => id + 1); + }, + } as unknown as AbortableAsyncState; + }, [error, value, loading]); +} diff --git a/x-pack/packages/kbn-ai-assistant/src/hooks/use_ai_assistant_app_service.ts b/x-pack/packages/kbn-ai-assistant/src/hooks/use_ai_assistant_app_service.ts new file mode 100644 index 0000000000000..bb1f93079eb09 --- /dev/null +++ b/x-pack/packages/kbn-ai-assistant/src/hooks/use_ai_assistant_app_service.ts @@ -0,0 +1,20 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { useKibana } from './use_kibana'; + +export function useAIAssistantAppService() { + const { services } = useKibana(); + + if (!services.observabilityAIAssistant?.service) { + throw new Error( + 'AI Assistant Service is not available. Did you provide this service in your plugin contract?' + ); + } + + return services.observabilityAIAssistant.service; +} diff --git a/x-pack/plugins/observability_solution/observability_ai_assistant_app/public/utils/get_settings_kb_href.ts b/x-pack/packages/kbn-ai-assistant/src/hooks/use_ai_assistant_chat_service.ts similarity index 52% rename from x-pack/plugins/observability_solution/observability_ai_assistant_app/public/utils/get_settings_kb_href.ts rename to x-pack/packages/kbn-ai-assistant/src/hooks/use_ai_assistant_chat_service.ts index 2aa625da08d17..a3eefef196901 100644 --- a/x-pack/plugins/observability_solution/observability_ai_assistant_app/public/utils/get_settings_kb_href.ts +++ b/x-pack/packages/kbn-ai-assistant/src/hooks/use_ai_assistant_chat_service.ts @@ -4,11 +4,12 @@ * 2.0; you may not use this file except in compliance with the Elastic License * 2.0. */ +import { useKibana } from './use_kibana'; -import { HttpStart } from '@kbn/core/public'; +export function useAIAssistantChatService() { + const { + services: { observabilityAIAssistant }, + } = useKibana(); -export function getSettingsKnowledgeBaseHref(http: HttpStart) { - return http!.basePath.prepend( - `/app/management/kibana/observabilityAiAssistantManagement?tab=knowledge_base` - ); + return observabilityAIAssistant.useObservabilityAIAssistantChatService(); } diff --git a/x-pack/plugins/observability_solution/observability_ai_assistant_app/public/hooks/use_confirm_modal.tsx b/x-pack/packages/kbn-ai-assistant/src/hooks/use_confirm_modal.tsx similarity index 100% rename from x-pack/plugins/observability_solution/observability_ai_assistant_app/public/hooks/use_confirm_modal.tsx rename to x-pack/packages/kbn-ai-assistant/src/hooks/use_confirm_modal.tsx diff --git a/x-pack/plugins/observability_solution/observability_ai_assistant_app/public/hooks/use_conversation.test.tsx b/x-pack/packages/kbn-ai-assistant/src/hooks/use_conversation.test.tsx similarity index 94% rename from x-pack/plugins/observability_solution/observability_ai_assistant_app/public/hooks/use_conversation.test.tsx rename to x-pack/packages/kbn-ai-assistant/src/hooks/use_conversation.test.tsx index 150847a011207..4c4ced36c8796 100644 --- a/x-pack/plugins/observability_solution/observability_ai_assistant_app/public/hooks/use_conversation.test.tsx +++ b/x-pack/packages/kbn-ai-assistant/src/hooks/use_conversation.test.tsx @@ -19,9 +19,8 @@ import { StreamingChatResponseEventType, StreamingChatResponseEventWithoutError, } from '@kbn/observability-ai-assistant-plugin/common'; -import { ObservabilityAIAssistantAppServiceProvider } from '../context/observability_ai_assistant_app_service_provider'; import { EMPTY_CONVERSATION_TITLE } from '../i18n'; -import type { ObservabilityAIAssistantAppService } from '../service/create_app_service'; +import type { AIAssistantAppService } from '../service/create_app_service'; import { useConversation, type UseConversationProps, @@ -35,9 +34,9 @@ import { KibanaContextProvider } from '@kbn/kibana-react-plugin/public'; let hookResult: RenderHookResult; -type MockedService = DeeplyMockedKeys> & { +type MockedService = DeeplyMockedKeys> & { conversations: DeeplyMockedKeys< - Omit + Omit > & { predefinedConversation$: Observable; }; @@ -66,18 +65,15 @@ const useKibanaMockServices = { uiSettings: { get: jest.fn(), }, - plugins: { - start: { - observabilityAIAssistant: { - useChat: createUseChat({ - notifications: { - toasts: { - addError: addErrorMock, - }, - } as unknown as NotificationsStart, - }), - }, - }, + observabilityAIAssistant: { + useChat: createUseChat({ + notifications: { + toasts: { + addError: addErrorMock, + }, + } as unknown as NotificationsStart, + }), + service: mockService, }, }; @@ -87,11 +83,7 @@ describe('useConversation', () => { beforeEach(() => { jest.clearAllMocks(); wrapper = ({ children }: PropsWithChildren) => ( - - - {children} - - + {children} ); }); diff --git a/x-pack/plugins/observability_solution/observability_ai_assistant_app/public/hooks/use_conversation.ts b/x-pack/packages/kbn-ai-assistant/src/hooks/use_conversation.ts similarity index 91% rename from x-pack/plugins/observability_solution/observability_ai_assistant_app/public/hooks/use_conversation.ts rename to x-pack/packages/kbn-ai-assistant/src/hooks/use_conversation.ts index 617b1b302473f..744e071d5b1ba 100644 --- a/x-pack/plugins/observability_solution/observability_ai_assistant_app/public/hooks/use_conversation.ts +++ b/x-pack/packages/kbn-ai-assistant/src/hooks/use_conversation.ts @@ -12,16 +12,14 @@ import type { ConversationCreateRequest, Message, } from '@kbn/observability-ai-assistant-plugin/common'; -import { - ObservabilityAIAssistantChatService, - useAbortableAsync, -} from '@kbn/observability-ai-assistant-plugin/public'; +import type { ObservabilityAIAssistantChatService } from '@kbn/observability-ai-assistant-plugin/public'; import type { AbortableAsyncState } from '@kbn/observability-ai-assistant-plugin/public'; import type { UseChatResult } from '@kbn/observability-ai-assistant-plugin/public'; import { EMPTY_CONVERSATION_TITLE } from '../i18n'; +import { useAIAssistantAppService } from './use_ai_assistant_app_service'; import { useKibana } from './use_kibana'; import { useOnce } from './use_once'; -import { useObservabilityAIAssistantAppService } from './use_observability_ai_assistant_app_service'; +import { useAbortableAsync } from './use_abortable_async'; function createNewConversation({ title = EMPTY_CONVERSATION_TITLE, @@ -62,17 +60,13 @@ export function useConversation({ connectorId, onConversationUpdate, }: UseConversationProps): UseConversationResult { - const service = useObservabilityAIAssistantAppService(); + const service = useAIAssistantAppService(); const { scope } = service; const { services: { notifications, - plugins: { - start: { - observabilityAIAssistant: { useChat }, - }, - }, + observabilityAIAssistant: { useChat }, }, } = useKibana(); @@ -106,8 +100,8 @@ export function useConversation({ }, }) .catch((err) => { - notifications.toasts.addError(err, { - title: i18n.translate('xpack.observabilityAiAssistant.errorUpdatingConversation', { + notifications!.toasts.addError(err, { + title: i18n.translate('xpack.aiAssistant.errorUpdatingConversation', { defaultMessage: 'Could not update conversation', }), }); diff --git a/x-pack/plugins/observability_solution/observability_ai_assistant_app/public/hooks/use_conversation_key.ts b/x-pack/packages/kbn-ai-assistant/src/hooks/use_conversation_key.ts similarity index 100% rename from x-pack/plugins/observability_solution/observability_ai_assistant_app/public/hooks/use_conversation_key.ts rename to x-pack/packages/kbn-ai-assistant/src/hooks/use_conversation_key.ts diff --git a/x-pack/plugins/observability_solution/observability_ai_assistant_app/public/hooks/use_conversation_list.ts b/x-pack/packages/kbn-ai-assistant/src/hooks/use_conversation_list.ts similarity index 86% rename from x-pack/plugins/observability_solution/observability_ai_assistant_app/public/hooks/use_conversation_list.ts rename to x-pack/packages/kbn-ai-assistant/src/hooks/use_conversation_list.ts index 6fa6bc02e7b35..d0db7665a30b6 100644 --- a/x-pack/plugins/observability_solution/observability_ai_assistant_app/public/hooks/use_conversation_list.ts +++ b/x-pack/packages/kbn-ai-assistant/src/hooks/use_conversation_list.ts @@ -12,9 +12,8 @@ import { type Conversation, useAbortableAsync, } from '@kbn/observability-ai-assistant-plugin/public'; +import { useAIAssistantAppService } from './use_ai_assistant_app_service'; import { useKibana } from './use_kibana'; -import { useObservabilityAIAssistantAppService } from './use_observability_ai_assistant_app_service'; - export interface UseConversationListResult { isLoading: boolean; conversations: AbortableAsyncState<{ conversations: Conversation[] }>; @@ -22,7 +21,7 @@ export interface UseConversationListResult { } export function useConversationList(): UseConversationListResult { - const service = useObservabilityAIAssistantAppService(); + const service = useAIAssistantAppService(); const [isUpdatingList, setIsUpdatingList] = useState(false); @@ -62,8 +61,8 @@ export function useConversationList(): UseConversationListResult { conversations.refresh(); } catch (err) { - notifications.toasts.addError(err, { - title: i18n.translate('xpack.observabilityAiAssistant.flyout.failedToDeleteConversation', { + notifications!.toasts.addError(err, { + title: i18n.translate('xpack.aiAssistant.flyout.failedToDeleteConversation', { defaultMessage: 'Could not delete conversation', }), }); diff --git a/x-pack/plugins/observability_solution/observability_ai_assistant_app/public/hooks/use_current_user.ts b/x-pack/packages/kbn-ai-assistant/src/hooks/use_current_user.ts similarity index 84% rename from x-pack/plugins/observability_solution/observability_ai_assistant_app/public/hooks/use_current_user.ts rename to x-pack/packages/kbn-ai-assistant/src/hooks/use_current_user.ts index 82c13eb876117..c169358653a49 100644 --- a/x-pack/plugins/observability_solution/observability_ai_assistant_app/public/hooks/use_current_user.ts +++ b/x-pack/packages/kbn-ai-assistant/src/hooks/use_current_user.ts @@ -5,9 +5,9 @@ * 2.0. */ +import { useKibana } from '@kbn/kibana-react-plugin/public'; import { AuthenticatedUser } from '@kbn/security-plugin/common'; import { useEffect, useState } from 'react'; -import { useKibana } from './use_kibana'; export function useCurrentUser() { const { @@ -19,7 +19,7 @@ export function useCurrentUser() { useEffect(() => { const getCurrentUser = async () => { try { - const authenticatedUser = await security.authc.getCurrentUser(); + const authenticatedUser = await security!.authc.getCurrentUser(); setUser(authenticatedUser); } catch { setUser(undefined); diff --git a/x-pack/plugins/observability_solution/observability_ai_assistant_app/public/hooks/use_genai_connectors.ts b/x-pack/packages/kbn-ai-assistant/src/hooks/use_genai_connectors.ts similarity index 66% rename from x-pack/plugins/observability_solution/observability_ai_assistant_app/public/hooks/use_genai_connectors.ts rename to x-pack/packages/kbn-ai-assistant/src/hooks/use_genai_connectors.ts index 1b105513a2323..642bf9488f186 100644 --- a/x-pack/plugins/observability_solution/observability_ai_assistant_app/public/hooks/use_genai_connectors.ts +++ b/x-pack/packages/kbn-ai-assistant/src/hooks/use_genai_connectors.ts @@ -5,16 +5,13 @@ * 2.0. */ -import { useKibana } from './use_kibana'; +import { useKibana } from '@kbn/kibana-react-plugin/public'; +import { AIAssistantPluginStartDependencies } from '../types'; export function useGenAIConnectors() { const { - services: { - plugins: { - start: { observabilityAIAssistant }, - }, - }, - } = useKibana(); + services: { observabilityAIAssistant }, + } = useKibana(); return observabilityAIAssistant.useGenAIConnectors(); } diff --git a/x-pack/plugins/observability_solution/observability_ai_assistant_app/public/hooks/use_json_editor_model.ts b/x-pack/packages/kbn-ai-assistant/src/hooks/use_json_editor_model.ts similarity index 92% rename from x-pack/plugins/observability_solution/observability_ai_assistant_app/public/hooks/use_json_editor_model.ts rename to x-pack/packages/kbn-ai-assistant/src/hooks/use_json_editor_model.ts index 6f4535d84acef..1b14c504d935d 100644 --- a/x-pack/plugins/observability_solution/observability_ai_assistant_app/public/hooks/use_json_editor_model.ts +++ b/x-pack/packages/kbn-ai-assistant/src/hooks/use_json_editor_model.ts @@ -7,7 +7,7 @@ import { useEffect, useMemo, useState } from 'react'; import { monaco } from '@kbn/monaco'; import { createInitializedObject } from '../utils/create_initialized_object'; -import { useObservabilityAIAssistantChatService } from './use_observability_ai_assistant_chat_service'; +import { useAIAssistantChatService } from './use_ai_assistant_chat_service'; import { safeJsonParse } from '../utils/safe_json_parse'; const { editor, languages, Uri } = monaco; @@ -19,7 +19,7 @@ export const useJsonEditorModel = ({ functionName: string | undefined; initialJson?: string | undefined; }) => { - const chatService = useObservabilityAIAssistantChatService(); + const chatService = useAIAssistantChatService(); const functionDefinition = chatService.getFunctions().find((func) => func.name === functionName); diff --git a/x-pack/packages/kbn-ai-assistant/src/hooks/use_kibana.ts b/x-pack/packages/kbn-ai-assistant/src/hooks/use_kibana.ts new file mode 100644 index 0000000000000..44aec48a06467 --- /dev/null +++ b/x-pack/packages/kbn-ai-assistant/src/hooks/use_kibana.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 { useKibana } from '@kbn/kibana-react-plugin/public'; +import { AIAssistantPluginStartDependencies } from '../types'; + +const useTypedKibana = () => useKibana(); + +export { useTypedKibana as useKibana }; diff --git a/x-pack/plugins/observability_solution/observability_ai_assistant_app/public/hooks/use_knowledge_base.tsx b/x-pack/packages/kbn-ai-assistant/src/hooks/use_knowledge_base.tsx similarity index 82% rename from x-pack/plugins/observability_solution/observability_ai_assistant_app/public/hooks/use_knowledge_base.tsx rename to x-pack/packages/kbn-ai-assistant/src/hooks/use_knowledge_base.tsx index bca9b38485695..0b949fcdbff0e 100644 --- a/x-pack/plugins/observability_solution/observability_ai_assistant_app/public/hooks/use_knowledge_base.tsx +++ b/x-pack/packages/kbn-ai-assistant/src/hooks/use_knowledge_base.tsx @@ -15,7 +15,7 @@ import { useAbortableAsync, } from '@kbn/observability-ai-assistant-plugin/public'; import { useKibana } from './use_kibana'; -import { useObservabilityAIAssistantAppService } from './use_observability_ai_assistant_app_service'; +import { useAIAssistantAppService } from './use_ai_assistant_app_service'; export interface UseKnowledgeBaseResult { status: AbortableAsyncState<{ @@ -31,13 +31,8 @@ export interface UseKnowledgeBaseResult { } export function useKnowledgeBase(): UseKnowledgeBaseResult { - const { - notifications: { toasts }, - plugins: { - start: { ml }, - }, - } = useKibana().services; - const service = useObservabilityAIAssistantAppService(); + const { notifications, ml } = useKibana().services; + const service = useAIAssistantAppService(); const status = useAbortableAsync( ({ signal }) => { @@ -75,8 +70,8 @@ export function useKnowledgeBase(): UseKnowledgeBaseResult { return install(); } setInstallError(error); - toasts.addError(error, { - title: i18n.translate('xpack.observabilityAiAssistant.errorSettingUpKnowledgeBase', { + notifications!.toasts.addError(error, { + title: i18n.translate('xpack.aiAssistant.errorSettingUpKnowledgeBase', { defaultMessage: 'Could not set up Knowledge Base', }), }); @@ -92,5 +87,5 @@ export function useKnowledgeBase(): UseKnowledgeBaseResult { isInstalling, installError, }; - }, [status, isInstalling, installError, service, ml.mlApi?.savedObjects, toasts]); + }, [status, isInstalling, installError, service, ml, notifications]); } diff --git a/x-pack/plugins/observability_solution/observability_ai_assistant_app/public/hooks/use_last_used_prompts.ts b/x-pack/packages/kbn-ai-assistant/src/hooks/use_last_used_prompts.ts similarity index 100% rename from x-pack/plugins/observability_solution/observability_ai_assistant_app/public/hooks/use_last_used_prompts.ts rename to x-pack/packages/kbn-ai-assistant/src/hooks/use_last_used_prompts.ts diff --git a/x-pack/packages/kbn-ai-assistant/src/hooks/use_license.ts b/x-pack/packages/kbn-ai-assistant/src/hooks/use_license.ts new file mode 100644 index 0000000000000..6d146274c7f4d --- /dev/null +++ b/x-pack/packages/kbn-ai-assistant/src/hooks/use_license.ts @@ -0,0 +1,36 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import type { ILicense, LicenseType } from '@kbn/licensing-plugin/public'; +import { useCallback } from 'react'; +import useObservable from 'react-use/lib/useObservable'; +import { useKibana } from './use_kibana'; + +interface UseLicenseReturnValue { + getLicense: () => ILicense | null; + hasAtLeast: (level: LicenseType) => boolean | undefined; +} + +export const useLicense = (): UseLicenseReturnValue => { + const { + services: { licensing }, + } = useKibana(); + + const license = useObservable(licensing.license$); + + return { + getLicense: () => license ?? null, + hasAtLeast: useCallback( + (level: LicenseType) => { + if (!license) return; + + return !!license && license.isAvailable && license.isActive && license.hasAtLeast(level); + }, + [license] + ), + }; +}; diff --git a/x-pack/plugins/observability_solution/observability_ai_assistant_app/public/hooks/use_license_management_locator.ts b/x-pack/packages/kbn-ai-assistant/src/hooks/use_license_management_locator.ts similarity index 89% rename from x-pack/plugins/observability_solution/observability_ai_assistant_app/public/hooks/use_license_management_locator.ts rename to x-pack/packages/kbn-ai-assistant/src/hooks/use_license_management_locator.ts index 1d5dd04203352..7e650affa2ca5 100644 --- a/x-pack/plugins/observability_solution/observability_ai_assistant_app/public/hooks/use_license_management_locator.ts +++ b/x-pack/packages/kbn-ai-assistant/src/hooks/use_license_management_locator.ts @@ -11,11 +11,7 @@ const LICENSE_MANAGEMENT_LOCATOR = 'LICENSE_MANAGEMENT_LOCATOR'; export const useLicenseManagementLocator = () => { const { - services: { - plugins: { - start: { share }, - }, - }, + services: { share }, } = useKibana(); const locator = share.url.locators.get(LICENSE_MANAGEMENT_LOCATOR); diff --git a/x-pack/packages/kbn-ai-assistant/src/hooks/use_local_storage.test.ts b/x-pack/packages/kbn-ai-assistant/src/hooks/use_local_storage.test.ts new file mode 100644 index 0000000000000..ab1d00392fdb9 --- /dev/null +++ b/x-pack/packages/kbn-ai-assistant/src/hooks/use_local_storage.test.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 { renderHook, act } from '@testing-library/react-hooks'; +import { useLocalStorage } from './use_local_storage'; + +describe('useLocalStorage', () => { + const key = 'testKey'; + const defaultValue = 'defaultValue'; + + beforeEach(() => { + localStorage.clear(); + }); + + it('should return the default value when local storage is empty', () => { + const { result } = renderHook(() => useLocalStorage(key, defaultValue)); + const [item] = result.current; + + expect(item).toBe(defaultValue); + }); + + it('should return the stored value when local storage has a value', () => { + const storedValue = 'storedValue'; + localStorage.setItem(key, JSON.stringify(storedValue)); + const { result } = renderHook(() => useLocalStorage(key, defaultValue)); + const [item] = result.current; + + expect(item).toBe(storedValue); + }); + + it('should save the value to local storage', () => { + const { result } = renderHook(() => useLocalStorage(key, defaultValue)); + const [, saveToStorage] = result.current; + const newValue = 'newValue'; + + act(() => { + saveToStorage(newValue); + }); + + expect(JSON.parse(localStorage.getItem(key) || '')).toBe(newValue); + }); + + it('should remove the value from local storage when the value is undefined', () => { + const { result } = renderHook(() => useLocalStorage(key, defaultValue)); + const [, saveToStorage] = result.current; + + act(() => { + saveToStorage(undefined as unknown as string); + }); + + expect(localStorage.getItem(key)).toBe(null); + }); + + it('should listen for storage events to window, and remove the listener upon unmount', () => { + const addEventListenerSpy = jest.spyOn(window, 'addEventListener'); + const removeEventListenerSpy = jest.spyOn(window, 'removeEventListener'); + + const { unmount } = renderHook(() => useLocalStorage(key, defaultValue)); + + expect(addEventListenerSpy).toHaveBeenCalled(); + + const eventTypes = addEventListenerSpy.mock.calls; + + expect(eventTypes).toContainEqual(['storage', expect.any(Function)]); + + unmount(); + + expect(removeEventListenerSpy).toHaveBeenCalled(); + + addEventListenerSpy.mockRestore(); + removeEventListenerSpy.mockRestore(); + }); +}); diff --git a/x-pack/packages/kbn-ai-assistant/src/hooks/use_local_storage.ts b/x-pack/packages/kbn-ai-assistant/src/hooks/use_local_storage.ts new file mode 100644 index 0000000000000..ea9e13163e4b0 --- /dev/null +++ b/x-pack/packages/kbn-ai-assistant/src/hooks/use_local_storage.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 { useState, useEffect, useMemo, useCallback } from 'react'; + +export function useLocalStorage(key: string, defaultValue: T) { + // This is necessary to fix a race condition issue. + // It guarantees that the latest value will be always returned after the value is updated + const [storageUpdate, setStorageUpdate] = useState(0); + + const item = useMemo(() => { + return getFromStorage(key, defaultValue); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [key, storageUpdate, defaultValue]); + + const saveToStorage = useCallback( + (value: T) => { + if (value === undefined) { + window.localStorage.removeItem(key); + } else { + window.localStorage.setItem(key, JSON.stringify(value)); + setStorageUpdate(storageUpdate + 1); + } + }, + [key, storageUpdate] + ); + + useEffect(() => { + function onUpdate(event: StorageEvent) { + if (event.key === key) { + setStorageUpdate(storageUpdate + 1); + } + } + window.addEventListener('storage', onUpdate); + return () => { + window.removeEventListener('storage', onUpdate); + }; + }, [key, setStorageUpdate, storageUpdate]); + + return useMemo(() => [item, saveToStorage] as const, [item, saveToStorage]); +} + +function getFromStorage(keyName: string, defaultValue: T) { + const storedItem = window.localStorage.getItem(keyName); + + if (storedItem !== null) { + try { + return JSON.parse(storedItem) as T; + } catch (err) { + window.localStorage.removeItem(keyName); + // eslint-disable-next-line no-console + console.log(`Unable to decode: ${keyName}`); + } + } + return defaultValue; +} diff --git a/x-pack/plugins/observability_solution/observability_ai_assistant_app/public/hooks/use_once.ts b/x-pack/packages/kbn-ai-assistant/src/hooks/use_once.ts similarity index 100% rename from x-pack/plugins/observability_solution/observability_ai_assistant_app/public/hooks/use_once.ts rename to x-pack/packages/kbn-ai-assistant/src/hooks/use_once.ts diff --git a/x-pack/plugins/observability_solution/observability_ai_assistant_app/public/hooks/use_simulated_function_calling.ts b/x-pack/packages/kbn-ai-assistant/src/hooks/use_simulated_function_calling.ts similarity index 89% rename from x-pack/plugins/observability_solution/observability_ai_assistant_app/public/hooks/use_simulated_function_calling.ts rename to x-pack/packages/kbn-ai-assistant/src/hooks/use_simulated_function_calling.ts index 4d441b03a3ddc..4515f2126dbfd 100644 --- a/x-pack/plugins/observability_solution/observability_ai_assistant_app/public/hooks/use_simulated_function_calling.ts +++ b/x-pack/packages/kbn-ai-assistant/src/hooks/use_simulated_function_calling.ts @@ -13,7 +13,7 @@ export function useSimulatedFunctionCalling() { services: { uiSettings }, } = useKibana(); - const simulatedFunctionCallingEnabled = uiSettings.get( + const simulatedFunctionCallingEnabled = uiSettings!.get( aiAssistantSimulatedFunctionCalling, false ); diff --git a/x-pack/packages/kbn-ai-assistant/src/i18n.ts b/x-pack/packages/kbn-ai-assistant/src/i18n.ts new file mode 100644 index 0000000000000..5c5be1633a07a --- /dev/null +++ b/x-pack/packages/kbn-ai-assistant/src/i18n.ts @@ -0,0 +1,20 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { i18n } from '@kbn/i18n'; + +export const ASSISTANT_SETUP_TITLE = i18n.translate('xpack.aiAssistant.assistantSetup.title', { + defaultMessage: 'Welcome to the Elastic AI Assistant', +}); + +export const EMPTY_CONVERSATION_TITLE = i18n.translate('xpack.aiAssistant.emptyConversationTitle', { + defaultMessage: 'New conversation', +}); + +export const UPGRADE_LICENSE_TITLE = i18n.translate('xpack.aiAssistant.incorrectLicense.title', { + defaultMessage: 'Upgrade your license', +}); diff --git a/x-pack/packages/kbn-ai-assistant/src/index.ts b/x-pack/packages/kbn-ai-assistant/src/index.ts new file mode 100644 index 0000000000000..ba2265e88715f --- /dev/null +++ b/x-pack/packages/kbn-ai-assistant/src/index.ts @@ -0,0 +1,11 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +export * from './conversation/conversation_view'; +export * from './service/create_app_service'; +export * from './hooks'; +export * from './chat'; diff --git a/x-pack/plugins/observability_solution/observability_ai_assistant_app/public/components/prompt_editor/prompt_editor.stories.tsx b/x-pack/packages/kbn-ai-assistant/src/prompt_editor/prompt_editor.stories.tsx similarity index 96% rename from x-pack/plugins/observability_solution/observability_ai_assistant_app/public/components/prompt_editor/prompt_editor.stories.tsx rename to x-pack/packages/kbn-ai-assistant/src/prompt_editor/prompt_editor.stories.tsx index f951653b152cc..ed2948e50f15e 100644 --- a/x-pack/plugins/observability_solution/observability_ai_assistant_app/public/components/prompt_editor/prompt_editor.stories.tsx +++ b/x-pack/packages/kbn-ai-assistant/src/prompt_editor/prompt_editor.stories.tsx @@ -8,13 +8,13 @@ import React from 'react'; import { ComponentStory, ComponentStoryObj } from '@storybook/react'; import { MessageRole } from '@kbn/observability-ai-assistant-plugin/public'; +import { KibanaReactStorybookDecorator } from '../utils/storybook_decorator.stories'; import { PromptEditor as Component, PromptEditorProps } from './prompt_editor'; -import { KibanaReactStorybookDecorator } from '../../utils/storybook_decorator.stories'; /* JSON Schema validation in the PromptEditor compponent does not work when rendering the component from within Storybook. - + */ export default { component: Component, diff --git a/x-pack/plugins/observability_solution/observability_ai_assistant_app/public/components/prompt_editor/prompt_editor.tsx b/x-pack/packages/kbn-ai-assistant/src/prompt_editor/prompt_editor.tsx similarity index 97% rename from x-pack/plugins/observability_solution/observability_ai_assistant_app/public/components/prompt_editor/prompt_editor.tsx rename to x-pack/packages/kbn-ai-assistant/src/prompt_editor/prompt_editor.tsx index db7f3a8f11888..cc2fe761d6176 100644 --- a/x-pack/plugins/observability_solution/observability_ai_assistant_app/public/components/prompt_editor/prompt_editor.tsx +++ b/x-pack/packages/kbn-ai-assistant/src/prompt_editor/prompt_editor.tsx @@ -14,10 +14,10 @@ import { type TelemetryEventTypeWithPayload, ObservabilityAIAssistantTelemetryEventType, } from '@kbn/observability-ai-assistant-plugin/public'; +import { useLastUsedPrompts } from '../hooks/use_last_used_prompts'; import { FunctionListPopover } from '../chat/function_list_popover'; import { PromptEditorFunction } from './prompt_editor_function'; import { PromptEditorNaturalLanguage } from './prompt_editor_natural_language'; -import { useLastUsedPrompts } from '../../hooks/use_last_used_prompts'; export interface PromptEditorProps { disabled: boolean; @@ -194,7 +194,7 @@ export function PromptEditor({ {functionName} {chatService.renderFunction(props.name, props.arguments, props.response, props.onActionClick)} diff --git a/x-pack/plugins/observability_solution/observability_ai_assistant_app/public/service/create_app_service.ts b/x-pack/packages/kbn-ai-assistant/src/service/create_app_service.ts similarity index 63% rename from x-pack/plugins/observability_solution/observability_ai_assistant_app/public/service/create_app_service.ts rename to x-pack/packages/kbn-ai-assistant/src/service/create_app_service.ts index dfb9b703bc4ed..bd01ab39a6d5c 100644 --- a/x-pack/plugins/observability_solution/observability_ai_assistant_app/public/service/create_app_service.ts +++ b/x-pack/packages/kbn-ai-assistant/src/service/create_app_service.ts @@ -6,15 +6,15 @@ */ import type { ObservabilityAIAssistantService } from '@kbn/observability-ai-assistant-plugin/public'; -import type { ObservabilityAIAssistantAppPluginStartDependencies } from '../types'; +import { AIAssistantPluginStartDependencies } from '../types'; -export type ObservabilityAIAssistantAppService = ObservabilityAIAssistantService; +export type AIAssistantAppService = ObservabilityAIAssistantService; export function createAppService({ pluginsStart, }: { - pluginsStart: ObservabilityAIAssistantAppPluginStartDependencies; -}): ObservabilityAIAssistantAppService { + pluginsStart: AIAssistantPluginStartDependencies; +}): AIAssistantAppService { return { ...pluginsStart.observabilityAIAssistant.service, }; diff --git a/x-pack/packages/kbn-ai-assistant/src/types/index.ts b/x-pack/packages/kbn-ai-assistant/src/types/index.ts new file mode 100644 index 0000000000000..afebbafd7e643 --- /dev/null +++ b/x-pack/packages/kbn-ai-assistant/src/types/index.ts @@ -0,0 +1,20 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import type { LicensingPluginStart } from '@kbn/licensing-plugin/public'; +import type { MlPluginStart } from '@kbn/ml-plugin/public'; +import type { ObservabilityAIAssistantPublicStart } from '@kbn/observability-ai-assistant-plugin/public'; +import type { SharePluginStart } from '@kbn/share-plugin/public'; +import type { TriggersAndActionsUIPublicPluginStart } from '@kbn/triggers-actions-ui-plugin/public'; + +export interface AIAssistantPluginStartDependencies { + licensing: LicensingPluginStart; + ml: MlPluginStart; + observabilityAIAssistant: ObservabilityAIAssistantPublicStart; + share: SharePluginStart; + triggersActionsUi: TriggersAndActionsUIPublicPluginStart; +} diff --git a/x-pack/plugins/observability_solution/observability_ai_assistant_app/public/utils/builders.ts b/x-pack/packages/kbn-ai-assistant/src/utils/builders.ts similarity index 100% rename from x-pack/plugins/observability_solution/observability_ai_assistant_app/public/utils/builders.ts rename to x-pack/packages/kbn-ai-assistant/src/utils/builders.ts diff --git a/x-pack/plugins/observability_solution/observability_ai_assistant_app/public/utils/create_initialized_object.test.ts b/x-pack/packages/kbn-ai-assistant/src/utils/create_initialized_object.test.ts similarity index 100% rename from x-pack/plugins/observability_solution/observability_ai_assistant_app/public/utils/create_initialized_object.test.ts rename to x-pack/packages/kbn-ai-assistant/src/utils/create_initialized_object.test.ts diff --git a/x-pack/plugins/observability_solution/observability_ai_assistant_app/public/utils/create_initialized_object.ts b/x-pack/packages/kbn-ai-assistant/src/utils/create_initialized_object.ts similarity index 100% rename from x-pack/plugins/observability_solution/observability_ai_assistant_app/public/utils/create_initialized_object.ts rename to x-pack/packages/kbn-ai-assistant/src/utils/create_initialized_object.ts diff --git a/x-pack/plugins/observability_solution/observability_ai_assistant_app/public/utils/create_mock_chat_service.ts b/x-pack/packages/kbn-ai-assistant/src/utils/create_mock_chat_service.ts similarity index 100% rename from x-pack/plugins/observability_solution/observability_ai_assistant_app/public/utils/create_mock_chat_service.ts rename to x-pack/packages/kbn-ai-assistant/src/utils/create_mock_chat_service.ts diff --git a/x-pack/plugins/observability_solution/observability_ai_assistant_app/public/utils/get_role_translation.ts b/x-pack/packages/kbn-ai-assistant/src/utils/get_role_translation.ts similarity index 61% rename from x-pack/plugins/observability_solution/observability_ai_assistant_app/public/utils/get_role_translation.ts rename to x-pack/packages/kbn-ai-assistant/src/utils/get_role_translation.ts index f74c9f842e402..95421a089dea0 100644 --- a/x-pack/plugins/observability_solution/observability_ai_assistant_app/public/utils/get_role_translation.ts +++ b/x-pack/packages/kbn-ai-assistant/src/utils/get_role_translation.ts @@ -10,21 +10,18 @@ import { MessageRole } from '@kbn/observability-ai-assistant-plugin/public'; export function getRoleTranslation(role: MessageRole) { if (role === MessageRole.User) { - return i18n.translate('xpack.observabilityAiAssistant.chatTimeline.messages.user.label', { + return i18n.translate('xpack.aiAssistant.chatTimeline.messages.user.label', { defaultMessage: 'You', }); } if (role === MessageRole.System) { - return i18n.translate('xpack.observabilityAiAssistant.chatTimeline.messages.system.label', { + return i18n.translate('xpack.aiAssistant.chatTimeline.messages.system.label', { defaultMessage: 'System', }); } - return i18n.translate( - 'xpack.observabilityAiAssistant.chatTimeline.messages.elasticAssistant.label', - { - defaultMessage: 'Elastic Assistant', - } - ); + return i18n.translate('xpack.aiAssistant.chatTimeline.messages.elasticAssistant.label', { + defaultMessage: 'Elastic Assistant', + }); } diff --git a/x-pack/plugins/observability_solution/observability_ai_assistant_app/public/utils/get_timeline_items_from_conversation.test.tsx b/x-pack/packages/kbn-ai-assistant/src/utils/get_timeline_items_from_conversation.test.tsx similarity index 98% rename from x-pack/plugins/observability_solution/observability_ai_assistant_app/public/utils/get_timeline_items_from_conversation.test.tsx rename to x-pack/packages/kbn-ai-assistant/src/utils/get_timeline_items_from_conversation.test.tsx index 6fb7e1a323d08..337c11419209e 100644 --- a/x-pack/plugins/observability_solution/observability_ai_assistant_app/public/utils/get_timeline_items_from_conversation.test.tsx +++ b/x-pack/packages/kbn-ai-assistant/src/utils/get_timeline_items_from_conversation.test.tsx @@ -23,12 +23,8 @@ function Providers({ children }: { children: React.ReactElement }) { mockChatService, - }, - }, + observabilityAIAssistant: { + useObservabilityAIAssistantChatService: () => mockChatService, }, }} > diff --git a/x-pack/plugins/observability_solution/observability_ai_assistant_app/public/utils/get_timeline_items_from_conversation.tsx b/x-pack/packages/kbn-ai-assistant/src/utils/get_timeline_items_from_conversation.tsx similarity index 94% rename from x-pack/plugins/observability_solution/observability_ai_assistant_app/public/utils/get_timeline_items_from_conversation.tsx rename to x-pack/packages/kbn-ai-assistant/src/utils/get_timeline_items_from_conversation.tsx index 9a3fed770b944..999ac4f095025 100644 --- a/x-pack/plugins/observability_solution/observability_ai_assistant_app/public/utils/get_timeline_items_from_conversation.tsx +++ b/x-pack/packages/kbn-ai-assistant/src/utils/get_timeline_items_from_conversation.tsx @@ -18,9 +18,9 @@ import { ObservabilityAIAssistantChatService, } from '@kbn/observability-ai-assistant-plugin/public'; import type { ChatActionClickPayload } from '@kbn/observability-ai-assistant-plugin/public'; -import type { ChatTimelineItem } from '../components/chat/chat_timeline'; -import { RenderFunction } from '../components/render_function'; +import { RenderFunction } from '../render_function'; import { safeJsonParse } from './safe_json_parse'; +import type { ChatTimelineItem } from '../chat/chat_timeline'; function convertMessageToMarkdownCodeBlock(message: Message['message']) { let value: object; @@ -95,7 +95,7 @@ export function getTimelineItemsfromConversation({ '@timestamp': new Date().toISOString(), message: { role: MessageRole.User }, }, - title: i18n.translate('xpack.observabilityAiAssistant.conversationStartTitle', { + title: i18n.translate('xpack.aiAssistant.conversationStartTitle', { defaultMessage: 'started a conversation', }), role: MessageRole.User, @@ -149,7 +149,7 @@ export function getTimelineItemsfromConversation({ title = !isError ? ( , @@ -157,7 +157,7 @@ export function getTimelineItemsfromConversation({ /> ) : ( , @@ -189,7 +189,7 @@ export function getTimelineItemsfromConversation({ // User suggested a function title = ( , @@ -222,7 +222,7 @@ export function getTimelineItemsfromConversation({ if (message.message.function_call?.name) { title = ( , diff --git a/x-pack/plugins/entity_manager/server/lib/entities/helpers/is_backfill_enabled.ts b/x-pack/packages/kbn-ai-assistant/src/utils/non_nullable.ts similarity index 57% rename from x-pack/plugins/entity_manager/server/lib/entities/helpers/is_backfill_enabled.ts rename to x-pack/packages/kbn-ai-assistant/src/utils/non_nullable.ts index 4c34f5d3c0256..8618e44dbb823 100644 --- a/x-pack/plugins/entity_manager/server/lib/entities/helpers/is_backfill_enabled.ts +++ b/x-pack/packages/kbn-ai-assistant/src/utils/non_nullable.ts @@ -5,8 +5,6 @@ * 2.0. */ -import { EntityDefinition } from '@kbn/entities-schema'; - -export function isBackfillEnabled(definition: EntityDefinition) { - return definition.history.settings.backfillSyncDelay != null; +export function nonNullable(v: T): v is NonNullable { + return v !== null && v !== undefined; } diff --git a/x-pack/plugins/observability_solution/observability_ai_assistant_app/public/utils/get_settings_href.ts b/x-pack/packages/kbn-ai-assistant/src/utils/safe_json_parse.ts similarity index 57% rename from x-pack/plugins/observability_solution/observability_ai_assistant_app/public/utils/get_settings_href.ts rename to x-pack/packages/kbn-ai-assistant/src/utils/safe_json_parse.ts index 45a3083d66327..a4f2dfa5c2503 100644 --- a/x-pack/plugins/observability_solution/observability_ai_assistant_app/public/utils/get_settings_href.ts +++ b/x-pack/packages/kbn-ai-assistant/src/utils/safe_json_parse.ts @@ -5,8 +5,10 @@ * 2.0. */ -import { HttpStart } from '@kbn/core/public'; - -export function getSettingsHref(http: HttpStart) { - return http!.basePath.prepend(`/app/management/kibana/observabilityAiAssistantManagement`); +export function safeJsonParse(jsonStr: string) { + try { + return JSON.parse(jsonStr); + } catch (err) { + return jsonStr; + } } diff --git a/x-pack/plugins/observability_solution/observability_ai_assistant_app/public/utils/storybook_decorator.stories.tsx b/x-pack/packages/kbn-ai-assistant/src/utils/storybook_decorator.stories.tsx similarity index 57% rename from x-pack/plugins/observability_solution/observability_ai_assistant_app/public/utils/storybook_decorator.stories.tsx rename to x-pack/packages/kbn-ai-assistant/src/utils/storybook_decorator.stories.tsx index 9dc2e7057b951..d6292803b42af 100644 --- a/x-pack/plugins/observability_solution/observability_ai_assistant_app/public/utils/storybook_decorator.stories.tsx +++ b/x-pack/packages/kbn-ai-assistant/src/utils/storybook_decorator.stories.tsx @@ -13,10 +13,9 @@ import { } from '@kbn/observability-ai-assistant-plugin/public'; import { Subject } from 'rxjs'; import { coreMock } from '@kbn/core/public/mocks'; -import { ObservabilityAIAssistantAppService } from '../service/create_app_service'; -import { ObservabilityAIAssistantAppServiceProvider } from '../context/observability_ai_assistant_app_service_provider'; +import { AIAssistantAppService } from '../service/create_app_service'; -const mockService: ObservabilityAIAssistantAppService = { +const mockService: AIAssistantAppService = { ...createStorybookService(), }; @@ -38,25 +37,17 @@ export function KibanaReactStorybookDecorator(Story: ComponentType) { licensing: { license$: new Subject(), }, - // observabilityAIAssistant: { - // ObservabilityAIAssistantChatServiceContext, - // ObservabilityAIAssistantMultipaneFlyoutContext, - // }, - plugins: { - start: { - observabilityAIAssistant: { - ObservabilityAIAssistantMultipaneFlyoutContext, - }, - triggersActionsUi: { getAddRuleFlyout: {}, getAddConnectorFlyout: {} }, - }, + observabilityAIAssistant: { + ObservabilityAIAssistantChatServiceContext, + ObservabilityAIAssistantMultipaneFlyoutContext, + service: mockService, }, + triggersActionsUi: { getAddRuleFlyout: {}, getAddConnectorFlyout: {} }, }} > - - - - - + + + ); } diff --git a/x-pack/packages/kbn-ai-assistant/tsconfig.json b/x-pack/packages/kbn-ai-assistant/tsconfig.json new file mode 100644 index 0000000000000..c8d91c9d37450 --- /dev/null +++ b/x-pack/packages/kbn-ai-assistant/tsconfig.json @@ -0,0 +1,39 @@ +{ + "extends": "../../../tsconfig.base.json", + "compilerOptions": { + "outDir": "target/types", + "types": [ + "jest", + "node", + "react", + "@emotion/react/types/css-prop" + ] + }, + "include": [ + "**/*.ts", + "**/*.tsx", + ], + "exclude": [ + "target/**/*" + ], + "kbn_references": [ + "@kbn/core-http-browser", + "@kbn/i18n", + "@kbn/triggers-actions-ui-plugin", + "@kbn/actions-plugin", + "@kbn/i18n-react", + "@kbn/ui-theme", + "@kbn/core", + "@kbn/observability-ai-assistant-plugin", + "@kbn/security-plugin", + "@kbn/user-profile-components", + "@kbn/std", + "@kbn/utility-types-jest", + "@kbn/kibana-react-plugin", + "@kbn/monaco", + "@kbn/licensing-plugin", + "@kbn/code-editor", + "@kbn/ml-plugin", + "@kbn/share-plugin", + ] +} diff --git a/x-pack/packages/kbn-cloud-security-posture-common/constants.ts b/x-pack/packages/kbn-cloud-security-posture-common/constants.ts index 7ff50efdd9489..a24d676dc6f88 100644 --- a/x-pack/packages/kbn-cloud-security-posture-common/constants.ts +++ b/x-pack/packages/kbn-cloud-security-posture-common/constants.ts @@ -36,6 +36,10 @@ export const CDR_LATEST_THIRD_PARTY_VULNERABILITIES_INDEX_PATTERN = export const CDR_VULNERABILITIES_INDEX_PATTERN = `${CDR_LATEST_THIRD_PARTY_VULNERABILITIES_INDEX_PATTERN},${CDR_LATEST_NATIVE_VULNERABILITIES_INDEX_PATTERN}`; export const LATEST_VULNERABILITIES_RETENTION_POLICY = '3d'; +// TODO: remove once https://github.com/elastic/security-team/issues/10801 is done +// meant as a temp workaround to get good enough posture view for 3rd party integrations, see https://github.com/elastic/security-team/issues/10683 +export const CDR_3RD_PARTY_RETENTION_POLICY = '90d'; + export const VULNERABILITIES_SEVERITY: Record = { LOW: 'LOW', MEDIUM: 'MEDIUM', diff --git a/x-pack/packages/kbn-cloud-security-posture-common/index.ts b/x-pack/packages/kbn-cloud-security-posture-common/index.ts index e01401f37ef23..d5ee781c39b20 100644 --- a/x-pack/packages/kbn-cloud-security-posture-common/index.ts +++ b/x-pack/packages/kbn-cloud-security-posture-common/index.ts @@ -18,7 +18,10 @@ export type { CspSetupStatus, } from './types/status'; export type { CspFinding, CspFindingResult } from './types/findings'; -export type { CspVulnerabilityFinding } from './schema/vulnerabilities/csp_vulnerability_finding'; +export type { + CspVulnerabilityFinding, + Vulnerability, +} from './schema/vulnerabilities/csp_vulnerability_finding'; export type { BenchmarksCisId } from './types/benchmark'; export type { VulnSeverity } from './types/vulnerabilities'; export * from './constants'; diff --git a/x-pack/packages/kbn-cloud-security-posture-common/utils/ui_metrics.ts b/x-pack/packages/kbn-cloud-security-posture-common/utils/ui_metrics.ts index 9ea12ef7ed45f..8ecedd744efef 100644 --- a/x-pack/packages/kbn-cloud-security-posture-common/utils/ui_metrics.ts +++ b/x-pack/packages/kbn-cloud-security-posture-common/utils/ui_metrics.ts @@ -22,6 +22,7 @@ export const VULNERABILITIES_FLYOUT_VISITS = 'vulnerabilities-flyout-visits'; export const OPEN_FINDINGS_FLYOUT = 'open-findings-flyout'; export const GROUP_BY_CLICK = 'group-by-click'; export const CHANGE_RULE_STATE = 'change-rule-state'; +export const ENTITY_FLYOUT_VULNERABILITY_VIEW_VISITS = 'entity-flyout-vulnerability-view-visits'; type CloudSecurityUiCounters = | typeof ENTITY_FLYOUT_MISCONFIGURATION_VIEW_VISITS @@ -32,6 +33,7 @@ type CloudSecurityUiCounters = | typeof CREATE_DETECTION_RULE_FROM_FLYOUT | typeof CREATE_DETECTION_FROM_TABLE_ROW_ACTION | typeof GROUP_BY_CLICK + | typeof ENTITY_FLYOUT_VULNERABILITY_VIEW_VISITS | typeof CHANGE_RULE_STATE; export class UiMetricService { diff --git a/x-pack/packages/kbn-cloud-security-posture/index.ts b/x-pack/packages/kbn-cloud-security-posture/index.ts index 73b77376db46c..b7e45a546f3d5 100644 --- a/x-pack/packages/kbn-cloud-security-posture/index.ts +++ b/x-pack/packages/kbn-cloud-security-posture/index.ts @@ -12,5 +12,7 @@ export type { NavFilter } from './src/hooks/use_navigate_findings'; export { showErrorToast } from './src/utils/show_error_toast'; export { encodeQuery, decodeQuery } from './src/utils/query_utils'; export { CspEvaluationBadge } from './src/components/csp_evaluation_badge'; -export { getSeverityStatusColor } from './src/utils/get_vulnerability_colors'; +export { getSeverityStatusColor, getCvsScoreColor } from './src/utils/get_vulnerability_colors'; export { getSeverityText } from './src/utils/get_vulnerability_text'; +export { getVulnerabilityStats, hasVulnerabilitiesData } from './src/utils/vulnerability_helpers'; +export { CVSScoreBadge, SeverityStatusBadge } from './src/components/vulnerability_badges'; diff --git a/x-pack/plugins/cloud_security_posture/public/components/vulnerability_badges.tsx b/x-pack/packages/kbn-cloud-security-posture/src/components/vulnerability_badges.tsx similarity index 90% rename from x-pack/plugins/cloud_security_posture/public/components/vulnerability_badges.tsx rename to x-pack/packages/kbn-cloud-security-posture/src/components/vulnerability_badges.tsx index 7d4095b4bd662..e13d33c0e11cb 100644 --- a/x-pack/plugins/cloud_security_posture/public/components/vulnerability_badges.tsx +++ b/x-pack/packages/kbn-cloud-security-posture/src/components/vulnerability_badges.tsx @@ -10,9 +10,9 @@ import React from 'react'; import { css } from '@emotion/react'; import { float } from '@elastic/elasticsearch/lib/api/typesWithBodyKey'; import type { VulnSeverity } from '@kbn/cloud-security-posture-common'; -import { getSeverityStatusColor } from '@kbn/cloud-security-posture'; -import { getCvsScoreColor } from '../common/utils/get_vulnerability_colors'; -import { VULNERABILITIES_CVSS_SCORE_BADGE_SUBJ } from './test_subjects'; +import { getCvsScoreColor, getSeverityStatusColor } from '../utils/get_vulnerability_colors'; + +const VULNERABILITIES_CVSS_SCORE_BADGE_SUBJ = 'vulnerabilities_cvss_score_badge'; interface CVSScoreBadgeProps { score?: float; diff --git a/x-pack/packages/kbn-cloud-security-posture/src/hooks/use_misconfiguration_preview.ts b/x-pack/packages/kbn-cloud-security-posture/src/hooks/use_misconfiguration_preview.ts index 4711cd752ee5f..75bd0d3952bd7 100644 --- a/x-pack/packages/kbn-cloud-security-posture/src/hooks/use_misconfiguration_preview.ts +++ b/x-pack/packages/kbn-cloud-security-posture/src/hooks/use_misconfiguration_preview.ts @@ -38,7 +38,7 @@ export const useMisconfigurationPreview = (options: UseCspOptions) => { params: buildMisconfigurationsFindingsQuery(options, rulesStates!), }) ); - if (!aggregations && !options.ignore_unavailable) + if (!aggregations && options.ignore_unavailable === false) throw new Error('expected aggregations to be defined'); return { count: getMisconfigurationAggregationCount(aggregations?.count?.buckets), diff --git a/x-pack/packages/kbn-cloud-security-posture/src/hooks/use_vulnerabilities_findings.ts b/x-pack/packages/kbn-cloud-security-posture/src/hooks/use_vulnerabilities_findings.ts new file mode 100644 index 0000000000000..ba13ec983893f --- /dev/null +++ b/x-pack/packages/kbn-cloud-security-posture/src/hooks/use_vulnerabilities_findings.ts @@ -0,0 +1,68 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ +import { useQuery } from '@tanstack/react-query'; +import { useKibana } from '@kbn/kibana-react-plugin/public'; +import { lastValueFrom } from 'rxjs'; +import type { IKibanaSearchResponse, IKibanaSearchRequest } from '@kbn/search-types'; +import { + SearchRequest, + SearchResponse, + AggregationsMultiBucketAggregateBase, + AggregationsStringRareTermsBucketKeys, +} from '@elastic/elasticsearch/lib/api/typesWithBodyKey'; +import type { CspVulnerabilityFinding } from '@kbn/cloud-security-posture-common/schema/vulnerabilities/latest'; +import type { CoreStart } from '@kbn/core/public'; +import type { CspClientPluginStartDeps, UseCspOptions } from '../../type'; +import { showErrorToast } from '../..'; +import { getVulnerabilitiesAggregationCount, getVulnerabilitiesQuery } from '../utils/hooks_utils'; + +type LatestFindingsRequest = IKibanaSearchRequest; +type LatestFindingsResponse = IKibanaSearchResponse< + SearchResponse +>; + +interface FindingsAggs { + count: AggregationsMultiBucketAggregateBase; +} + +export const useVulnerabilitiesFindings = (options: UseCspOptions) => { + const { + data, + notifications: { toasts }, + } = useKibana().services; + /** + * We're using useInfiniteQuery in this case to allow the user to fetch more data (if available and up to 10k) + * useInfiniteQuery differs from useQuery because it accumulates and caches a chunk of data from the previous fetches into an array + * it uses the getNextPageParam to know if there are more pages to load and retrieve the position of + * the last loaded record to be used as a from parameter to fetch the next chunk of data. + */ + return useQuery( + ['csp_vulnerabilities_findings', { params: options }], + async ({ pageParam }) => { + const { + rawResponse: { aggregations, hits }, + } = await lastValueFrom( + data.search.search({ + params: getVulnerabilitiesQuery(options, pageParam), + }) + ); + + return { + count: getVulnerabilitiesAggregationCount(aggregations?.count?.buckets), + rows: hits.hits.map((finding) => ({ + vulnerability: finding._source?.vulnerability, + resource: finding._source?.resource, + })) as Array>, + }; + }, + { + keepPreviousData: true, + enabled: options.enabled, + onError: (err: Error) => showErrorToast(toasts, err), + } + ); +}; diff --git a/x-pack/packages/kbn-cloud-security-posture/src/hooks/use_vulnerabilities_preview.ts b/x-pack/packages/kbn-cloud-security-posture/src/hooks/use_vulnerabilities_preview.ts index 00ca9691b013f..82b3f41d26819 100644 --- a/x-pack/packages/kbn-cloud-security-posture/src/hooks/use_vulnerabilities_preview.ts +++ b/x-pack/packages/kbn-cloud-security-posture/src/hooks/use_vulnerabilities_preview.ts @@ -14,18 +14,11 @@ import { AggregationsMultiBucketAggregateBase, AggregationsStringRareTermsBucketKeys, } from '@elastic/elasticsearch/lib/api/typesWithBodyKey'; -import { - CDR_VULNERABILITIES_INDEX_PATTERN, - LATEST_VULNERABILITIES_RETENTION_POLICY, -} from '@kbn/cloud-security-posture-common'; import type { CspVulnerabilityFinding } from '@kbn/cloud-security-posture-common/schema/vulnerabilities/latest'; import type { CoreStart } from '@kbn/core/public'; import type { CspClientPluginStartDeps, UseCspOptions } from '../../type'; import { showErrorToast } from '../..'; -import { - getFindingsCountAggQueryVulnerabilities, - getVulnerabilitiesAggregationCount, -} from '../utils/hooks_utils'; +import { getVulnerabilitiesAggregationCount, getVulnerabilitiesQuery } from '../utils/hooks_utils'; type LatestFindingsRequest = IKibanaSearchRequest; type LatestFindingsResponse = IKibanaSearchResponse< @@ -36,30 +29,6 @@ interface FindingsAggs { count: AggregationsMultiBucketAggregateBase; } -const getVulnerabilitiesQuery = ({ query }: UseCspOptions, isPreview = false) => ({ - index: CDR_VULNERABILITIES_INDEX_PATTERN, - size: 0, - aggs: getFindingsCountAggQueryVulnerabilities(), - ignore_unavailable: true, - query: { - ...query, - bool: { - ...query?.bool, - filter: [ - ...(query?.bool?.filter ?? []), - { - range: { - '@timestamp': { - gte: `now-${LATEST_VULNERABILITIES_RETENTION_POLICY}`, - lte: 'now', - }, - }, - }, - ], - }, - }, -}); - export const useVulnerabilitiesPreview = (options: UseCspOptions) => { const { data, @@ -73,7 +42,7 @@ export const useVulnerabilitiesPreview = (options: UseCspOptions) => { rawResponse: { aggregations }, } = await lastValueFrom( data.search.search({ - params: getVulnerabilitiesQuery(options), + params: getVulnerabilitiesQuery(options, true), }) ); diff --git a/x-pack/packages/kbn-cloud-security-posture/src/utils/get_vulnerabilitiy_colors.test.ts b/x-pack/packages/kbn-cloud-security-posture/src/utils/get_vulnerabilitiy_colors.test.ts index 0516faa7e83f7..dcc506fd6b27d 100644 --- a/x-pack/packages/kbn-cloud-security-posture/src/utils/get_vulnerabilitiy_colors.test.ts +++ b/x-pack/packages/kbn-cloud-security-posture/src/utils/get_vulnerabilitiy_colors.test.ts @@ -6,7 +6,7 @@ */ import { euiThemeVars } from '@kbn/ui-theme'; -import { getSeverityStatusColor } from './get_vulnerability_colors'; +import { getCvsScoreColor, getSeverityStatusColor } from './get_vulnerability_colors'; describe('getSeverityStatusColor', () => { it('should return the correct color for LOW severity', () => { expect(getSeverityStatusColor('LOW')).toBe(euiThemeVars.euiColorVis0); @@ -28,3 +28,25 @@ describe('getSeverityStatusColor', () => { expect(getSeverityStatusColor('UNKNOWN')).toBe('#aaa'); }); }); + +describe('getCvsScoreColor', () => { + it('returns correct color for low severity score', () => { + expect(getCvsScoreColor(1.5)).toBe(euiThemeVars.euiColorVis0); + }); + + it('returns correct color for medium severity score', () => { + expect(getCvsScoreColor(5.5)).toBe(euiThemeVars.euiColorVis7); + }); + + it('returns correct color for high severity score', () => { + expect(getCvsScoreColor(7.9)).toBe(euiThemeVars.euiColorVis9); + }); + + it('returns correct color for critical severity score', () => { + expect(getCvsScoreColor(10.0)).toBe(euiThemeVars.euiColorDanger); + }); + + it('returns correct color for low severity score for undefined value', () => { + expect(getCvsScoreColor(-0.2)).toBe(euiThemeVars.euiColorVis0); + }); +}); diff --git a/x-pack/packages/kbn-cloud-security-posture/src/utils/get_vulnerability_colors.ts b/x-pack/packages/kbn-cloud-security-posture/src/utils/get_vulnerability_colors.ts index 7e651f790fd80..54bcb357137b7 100644 --- a/x-pack/packages/kbn-cloud-security-posture/src/utils/get_vulnerability_colors.ts +++ b/x-pack/packages/kbn-cloud-security-posture/src/utils/get_vulnerability_colors.ts @@ -9,6 +9,18 @@ import { euiThemeVars } from '@kbn/ui-theme'; import type { VulnSeverity } from '@kbn/cloud-security-posture-common'; import { VULNERABILITIES_SEVERITY } from '@kbn/cloud-security-posture-common'; +export const getCvsScoreColor = (score: number): string | undefined => { + if (score <= 4) { + return euiThemeVars.euiColorVis0; // low severity + } else if (score >= 4 && score <= 7) { + return euiThemeVars.euiColorVis7; // medium severity + } else if (score >= 7 && score <= 9) { + return euiThemeVars.euiColorVis9; // high severity + } else if (score >= 9) { + return euiThemeVars.euiColorDanger; // critical severity + } +}; + export const getSeverityStatusColor = (severity: VulnSeverity): string => { switch (severity) { case VULNERABILITIES_SEVERITY.LOW: diff --git a/x-pack/packages/kbn-cloud-security-posture/src/utils/hooks_utils.ts b/x-pack/packages/kbn-cloud-security-posture/src/utils/hooks_utils.ts index d99fac8d6d96e..d06f4efbde026 100644 --- a/x-pack/packages/kbn-cloud-security-posture/src/utils/hooks_utils.ts +++ b/x-pack/packages/kbn-cloud-security-posture/src/utils/hooks_utils.ts @@ -8,7 +8,8 @@ import type * as estypes from '@elastic/elasticsearch/lib/api/typesWithBodyKey'; import { CDR_MISCONFIGURATIONS_INDEX_PATTERN, - LATEST_FINDINGS_RETENTION_POLICY, + CDR_VULNERABILITIES_INDEX_PATTERN, + CDR_3RD_PARTY_RETENTION_POLICY, } from '@kbn/cloud-security-posture-common'; import type { CspBenchmarkRulesStates } from '@kbn/cloud-security-posture-common/schema/rules/latest'; import { buildMutedRulesFilter } from '@kbn/cloud-security-posture-common'; @@ -101,7 +102,7 @@ const buildMisconfigurationsFindingsQueryWithFilters = ( { range: { '@timestamp': { - gte: `now-${LATEST_FINDINGS_RETENTION_POLICY}`, + gte: `now-${CDR_3RD_PARTY_RETENTION_POLICY}`, lte: 'now', }, }, @@ -161,3 +162,31 @@ export const getFindingsCountAggQueryVulnerabilities = () => ({ }, }, }); + +export const getVulnerabilitiesQuery = ({ query }: UseCspOptions, isPreview = false) => ({ + index: CDR_VULNERABILITIES_INDEX_PATTERN, + size: isPreview ? 0 : 500, + aggs: getFindingsCountAggQueryVulnerabilities(), + ignore_unavailable: true, + query: buildVulnerabilityFindingsQueryWithFilters(query), +}); + +const buildVulnerabilityFindingsQueryWithFilters = (query: UseCspOptions['query']) => { + return { + ...query, + bool: { + ...query?.bool, + filter: [ + ...(query?.bool?.filter ?? []), + { + range: { + '@timestamp': { + gte: `now-${CDR_3RD_PARTY_RETENTION_POLICY}`, + lte: 'now', + }, + }, + }, + ], + }, + }; +}; diff --git a/x-pack/packages/kbn-cloud-security-posture/src/utils/vulnerability_helpers.test.ts b/x-pack/packages/kbn-cloud-security-posture/src/utils/vulnerability_helpers.test.ts new file mode 100644 index 0000000000000..898f9990a1a96 --- /dev/null +++ b/x-pack/packages/kbn-cloud-security-posture/src/utils/vulnerability_helpers.test.ts @@ -0,0 +1,74 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { euiThemeVars } from '@kbn/ui-theme'; +import { getVulnerabilityStats } from './vulnerability_helpers'; +import { i18n } from '@kbn/i18n'; + +describe('getVulnerabilitiesAggregationCount', () => { + it('should return empty array when all severity count is 0', () => { + const result = getVulnerabilityStats({ critical: 0, high: 0, medium: 0, low: 0, none: 0 }); + expect(result).toEqual([]); + }); + + it('should return stats for low, medium, high, and critical vulnerabilities', () => { + const result = getVulnerabilityStats({ critical: 1, high: 2, medium: 3, low: 4, none: 5 }); + + expect(result).toEqual([ + { + key: i18n.translate( + 'xpack.securitySolution.flyout.right.insights.vulnerabilities.noneVulnerabilitiesText', + { + defaultMessage: 'None', + } + ), + count: 5, + color: '#aaa', + }, + { + key: i18n.translate( + 'xpack.securitySolution.flyout.right.insights.vulnerabilities.lowVulnerabilitiesText', + { + defaultMessage: 'Low', + } + ), + count: 4, + color: euiThemeVars.euiColorVis0, + }, + { + key: i18n.translate( + 'xpack.securitySolution.flyout.right.insights.vulnerabilities.mediumVulnerabilitiesText', + { + defaultMessage: 'Medium', + } + ), + count: 3, + color: euiThemeVars.euiColorVis5_behindText, + }, + { + key: i18n.translate( + 'xpack.securitySolution.flyout.right.insights.vulnerabilities.highVulnerabilitiesText', + { + defaultMessage: 'High', + } + ), + count: 2, + color: euiThemeVars.euiColorVis9_behindText, + }, + { + key: i18n.translate( + 'xpack.securitySolution.flyout.right.insights.vulnerabilities.CriticalVulnerabilitiesText', + { + defaultMessage: 'Critical', + } + ), + count: 1, + color: euiThemeVars.euiColorDanger, + }, + ]); + }); +}); diff --git a/x-pack/packages/kbn-cloud-security-posture/src/utils/vulnerability_helpers.ts b/x-pack/packages/kbn-cloud-security-posture/src/utils/vulnerability_helpers.ts new file mode 100644 index 0000000000000..c8782daf35308 --- /dev/null +++ b/x-pack/packages/kbn-cloud-security-posture/src/utils/vulnerability_helpers.ts @@ -0,0 +1,101 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { VULNERABILITIES_SEVERITY } from '@kbn/cloud-security-posture-common'; +import { i18n } from '@kbn/i18n'; +import { getSeverityStatusColor } from './get_vulnerability_colors'; +import { getSeverityText } from './get_vulnerability_text'; + +interface VulnerabilitiesDistributionBarProps { + key: string; + count: number; + color: string; +} + +interface VulnerabilityCounts { + critical: number; + high: number; + medium: number; + low: number; + none: number; +} + +export const hasVulnerabilitiesData = (counts: VulnerabilityCounts): boolean => { + if (Object.values(counts).reduce((acc, value) => acc + value, 0) > 0) return true; + return false; +}; + +export const getVulnerabilityStats = ( + counts: VulnerabilityCounts +): VulnerabilitiesDistributionBarProps[] => { + const vulnerabilityStats: VulnerabilitiesDistributionBarProps[] = []; + + const levels = Object.values(counts); + + if (levels.every((level) => level === 0)) { + return vulnerabilityStats; + } + + if (counts.none > 0) + vulnerabilityStats.push({ + key: i18n.translate( + 'xpack.securitySolution.flyout.right.insights.vulnerabilities.noneVulnerabilitiesText', + { + defaultMessage: getSeverityText(VULNERABILITIES_SEVERITY.UNKNOWN), + } + ), + count: counts.none, + color: getSeverityStatusColor(VULNERABILITIES_SEVERITY.UNKNOWN), + }); + if (counts.low > 0) + vulnerabilityStats.push({ + key: i18n.translate( + 'xpack.securitySolution.flyout.right.insights.vulnerabilities.lowVulnerabilitiesText', + { + defaultMessage: getSeverityText(VULNERABILITIES_SEVERITY.LOW), + } + ), + count: counts.low, + color: getSeverityStatusColor(VULNERABILITIES_SEVERITY.LOW), + }); + + if (counts.medium > 0) + vulnerabilityStats.push({ + key: i18n.translate( + 'xpack.securitySolution.flyout.right.insights.vulnerabilities.mediumVulnerabilitiesText', + { + defaultMessage: getSeverityText(VULNERABILITIES_SEVERITY.MEDIUM), + } + ), + count: counts.medium, + color: getSeverityStatusColor(VULNERABILITIES_SEVERITY.MEDIUM), + }); + if (counts.high > 0) + vulnerabilityStats.push({ + key: i18n.translate( + 'xpack.securitySolution.flyout.right.insights.vulnerabilities.highVulnerabilitiesText', + { + defaultMessage: getSeverityText(VULNERABILITIES_SEVERITY.HIGH), + } + ), + count: counts.high, + color: getSeverityStatusColor(VULNERABILITIES_SEVERITY.HIGH), + }); + if (counts.critical > 0) + vulnerabilityStats.push({ + key: i18n.translate( + 'xpack.securitySolution.flyout.right.insights.vulnerabilities.CriticalVulnerabilitiesText', + { + defaultMessage: getSeverityText(VULNERABILITIES_SEVERITY.CRITICAL), + } + ), + count: counts.critical, + color: getSeverityStatusColor(VULNERABILITIES_SEVERITY.CRITICAL), + }); + + return vulnerabilityStats; +}; diff --git a/x-pack/packages/kbn-cloud-security-posture/tsconfig.json b/x-pack/packages/kbn-cloud-security-posture/tsconfig.json index 633ef28bf2074..a4a5376009d9a 100644 --- a/x-pack/packages/kbn-cloud-security-posture/tsconfig.json +++ b/x-pack/packages/kbn-cloud-security-posture/tsconfig.json @@ -5,7 +5,8 @@ "types": [ "jest", "node", - "react" + "react", + "@emotion/react/types/css-prop" ] }, "include": [ diff --git a/x-pack/packages/kbn-elastic-assistant-common/docs/openapi/ess/elastic_assistant_api_2023_10_31.bundled.schema.yaml b/x-pack/packages/kbn-elastic-assistant-common/docs/openapi/ess/elastic_assistant_api_2023_10_31.bundled.schema.yaml index 1e070b75322d4..8f80e61c07040 100644 --- a/x-pack/packages/kbn-elastic-assistant-common/docs/openapi/ess/elastic_assistant_api_2023_10_31.bundled.schema.yaml +++ b/x-pack/packages/kbn-elastic-assistant-common/docs/openapi/ess/elastic_assistant_api_2023_10_31.bundled.schema.yaml @@ -1194,6 +1194,7 @@ components: enum: - OpenAI - Azure OpenAI + - Other type: string Reader: additionalProperties: true diff --git a/x-pack/packages/kbn-elastic-assistant-common/docs/openapi/serverless/elastic_assistant_api_2023_10_31.bundled.schema.yaml b/x-pack/packages/kbn-elastic-assistant-common/docs/openapi/serverless/elastic_assistant_api_2023_10_31.bundled.schema.yaml index e13d7a05af41f..97c18a2f77b6e 100644 --- a/x-pack/packages/kbn-elastic-assistant-common/docs/openapi/serverless/elastic_assistant_api_2023_10_31.bundled.schema.yaml +++ b/x-pack/packages/kbn-elastic-assistant-common/docs/openapi/serverless/elastic_assistant_api_2023_10_31.bundled.schema.yaml @@ -1194,6 +1194,7 @@ components: enum: - OpenAI - Azure OpenAI + - Other type: string Reader: additionalProperties: true diff --git a/x-pack/packages/kbn-elastic-assistant-common/impl/schemas/conversations/common_attributes.gen.ts b/x-pack/packages/kbn-elastic-assistant-common/impl/schemas/conversations/common_attributes.gen.ts index 1ba701474b1f8..1dad26e1628db 100644 --- a/x-pack/packages/kbn-elastic-assistant-common/impl/schemas/conversations/common_attributes.gen.ts +++ b/x-pack/packages/kbn-elastic-assistant-common/impl/schemas/conversations/common_attributes.gen.ts @@ -46,7 +46,7 @@ export const Reader = z.object({}).catchall(z.unknown()); * Provider */ export type Provider = z.infer; -export const Provider = z.enum(['OpenAI', 'Azure OpenAI']); +export const Provider = z.enum(['OpenAI', 'Azure OpenAI', 'Other']); export type ProviderEnum = typeof Provider.enum; export const ProviderEnum = Provider.enum; diff --git a/x-pack/packages/kbn-elastic-assistant-common/impl/schemas/conversations/common_attributes.schema.yaml b/x-pack/packages/kbn-elastic-assistant-common/impl/schemas/conversations/common_attributes.schema.yaml index f6a8189182474..20423236f7423 100644 --- a/x-pack/packages/kbn-elastic-assistant-common/impl/schemas/conversations/common_attributes.schema.yaml +++ b/x-pack/packages/kbn-elastic-assistant-common/impl/schemas/conversations/common_attributes.schema.yaml @@ -34,6 +34,7 @@ components: enum: - OpenAI - Azure OpenAI + - Other MessageRole: type: string diff --git a/x-pack/packages/kbn-elastic-assistant-common/impl/schemas/knowledge_base/crud_kb_route.gen.ts b/x-pack/packages/kbn-elastic-assistant-common/impl/schemas/knowledge_base/crud_kb_route.gen.ts index 4eb41f0c1f136..fd599f5798cdc 100644 --- a/x-pack/packages/kbn-elastic-assistant-common/impl/schemas/knowledge_base/crud_kb_route.gen.ts +++ b/x-pack/packages/kbn-elastic-assistant-common/impl/schemas/knowledge_base/crud_kb_route.gen.ts @@ -76,7 +76,6 @@ export type ReadKnowledgeBaseRequestParamsInput = z.input; export const ReadKnowledgeBaseResponse = z.object({ elser_exists: z.boolean().optional(), - esql_exists: z.boolean().optional(), index_exists: z.boolean().optional(), is_setup_available: z.boolean().optional(), is_setup_in_progress: z.boolean().optional(), diff --git a/x-pack/packages/kbn-elastic-assistant-common/impl/schemas/knowledge_base/crud_kb_route.schema.yaml b/x-pack/packages/kbn-elastic-assistant-common/impl/schemas/knowledge_base/crud_kb_route.schema.yaml index 07d271e860756..a61e98602ab40 100644 --- a/x-pack/packages/kbn-elastic-assistant-common/impl/schemas/knowledge_base/crud_kb_route.schema.yaml +++ b/x-pack/packages/kbn-elastic-assistant-common/impl/schemas/knowledge_base/crud_kb_route.schema.yaml @@ -68,8 +68,6 @@ paths: properties: elser_exists: type: boolean - esql_exists: - type: boolean index_exists: type: boolean is_setup_available: diff --git a/x-pack/packages/kbn-elastic-assistant-common/impl/schemas/knowledge_base/entries/common_attributes.gen.ts b/x-pack/packages/kbn-elastic-assistant-common/impl/schemas/knowledge_base/entries/common_attributes.gen.ts index 1af5c46b1c130..c32517fec0860 100644 --- a/x-pack/packages/kbn-elastic-assistant-common/impl/schemas/knowledge_base/entries/common_attributes.gen.ts +++ b/x-pack/packages/kbn-elastic-assistant-common/impl/schemas/knowledge_base/entries/common_attributes.gen.ts @@ -106,7 +106,11 @@ export type BaseCreateProps = z.infer; export const BaseCreateProps = BaseRequiredFields.merge(BaseDefaultableFields); export type BaseUpdateProps = z.infer; -export const BaseUpdateProps = BaseCreateProps.partial(); +export const BaseUpdateProps = BaseCreateProps.partial().merge( + z.object({ + id: NonEmptyString, + }) +); export type BaseResponseProps = z.infer; export const BaseResponseProps = BaseRequiredFields.merge(BaseDefaultableFields.required()); diff --git a/x-pack/packages/kbn-elastic-assistant-common/impl/schemas/knowledge_base/entries/common_attributes.schema.yaml b/x-pack/packages/kbn-elastic-assistant-common/impl/schemas/knowledge_base/entries/common_attributes.schema.yaml index c1c551059f04b..af7f4dd8e4221 100644 --- a/x-pack/packages/kbn-elastic-assistant-common/impl/schemas/knowledge_base/entries/common_attributes.schema.yaml +++ b/x-pack/packages/kbn-elastic-assistant-common/impl/schemas/knowledge_base/entries/common_attributes.schema.yaml @@ -112,6 +112,12 @@ components: allOf: - $ref: "#/components/schemas/BaseCreateProps" x-modify: partial + - type: object + properties: + id: + $ref: "../../common_attributes.schema.yaml#/components/schemas/NonEmptyString" + required: + - id BaseResponseProps: x-inline: true diff --git a/x-pack/packages/kbn-elastic-assistant/impl/assistant/api/knowledge_base/use_knowledge_base_status.test.tsx b/x-pack/packages/kbn-elastic-assistant/impl/assistant/api/knowledge_base/use_knowledge_base_status.test.tsx index aaad50afacd91..80ce3d27d8dcb 100644 --- a/x-pack/packages/kbn-elastic-assistant/impl/assistant/api/knowledge_base/use_knowledge_base_status.test.tsx +++ b/x-pack/packages/kbn-elastic-assistant/impl/assistant/api/knowledge_base/use_knowledge_base_status.test.tsx @@ -32,7 +32,6 @@ jest.mock('@tanstack/react-query', () => ({ const statusResponse = { elser_exists: true, - esql_exists: true, index_exists: true, pipeline_exists: true, }; diff --git a/x-pack/packages/kbn-elastic-assistant/impl/assistant/api/knowledge_base/use_knowledge_base_status.tsx b/x-pack/packages/kbn-elastic-assistant/impl/assistant/api/knowledge_base/use_knowledge_base_status.tsx index 7f248e1c4c260..75e78f2a06948 100644 --- a/x-pack/packages/kbn-elastic-assistant/impl/assistant/api/knowledge_base/use_knowledge_base_status.tsx +++ b/x-pack/packages/kbn-elastic-assistant/impl/assistant/api/knowledge_base/use_knowledge_base_status.tsx @@ -89,7 +89,6 @@ export const useInvalidateKnowledgeBaseStatus = () => { export const isKnowledgeBaseSetup = (kbStatus: ReadKnowledgeBaseResponse | undefined): boolean => { return ( (kbStatus?.elser_exists && - kbStatus?.esql_exists && kbStatus?.security_labs_exists && kbStatus?.index_exists && kbStatus?.pipeline_exists) ?? diff --git a/x-pack/packages/kbn-elastic-assistant/impl/assistant/assistant_header/index.tsx b/x-pack/packages/kbn-elastic-assistant/impl/assistant/assistant_header/index.tsx index d81a56fb97eef..ef37506f2af17 100644 --- a/x-pack/packages/kbn-elastic-assistant/impl/assistant/assistant_header/index.tsx +++ b/x-pack/packages/kbn-elastic-assistant/impl/assistant/assistant_header/index.tsx @@ -5,16 +5,13 @@ * 2.0. */ -import React, { useState, useMemo, useCallback } from 'react'; +import React, { useMemo, useCallback } from 'react'; import { QueryObserverResult, RefetchOptions, RefetchQueryFilters } from '@tanstack/react-query'; import { EuiFlexGroup, EuiFlexItem, - EuiPopover, - EuiContextMenu, EuiButtonIcon, EuiPanel, - EuiConfirmModal, EuiToolTip, EuiSkeletonTitle, } from '@elastic/eui'; @@ -29,6 +26,7 @@ import { FlyoutNavigation } from '../assistant_overlay/flyout_navigation'; import { AssistantSettingsButton } from '../settings/assistant_settings_button'; import * as i18n from './translations'; import { AIConnector } from '../../connectorland/connector_selector'; +import { SettingsContextMenu } from '../settings/settings_context_menu/settings_context_menu'; interface OwnProps { selectedConversation: Conversation | undefined; @@ -94,21 +92,6 @@ export const AssistantHeader: React.FC = ({ [selectedConversation?.apiConfig?.connectorId] ); - const [isPopoverOpen, setPopover] = useState(false); - - const onButtonClick = useCallback(() => { - setPopover(!isPopoverOpen); - }, [isPopoverOpen]); - - const closePopover = useCallback(() => { - setPopover(false); - }, []); - - const [isResetConversationModalVisible, setIsResetConversationModalVisible] = useState(false); - - const closeDestroyModal = useCallback(() => setIsResetConversationModalVisible(false), []); - const showDestroyModal = useCallback(() => setIsResetConversationModalVisible(true), []); - const onConversationChange = useCallback( (updatedConversation: Conversation) => { onConversationSelected({ @@ -119,32 +102,6 @@ export const AssistantHeader: React.FC = ({ [onConversationSelected] ); - const panels = useMemo( - () => [ - { - id: 0, - items: [ - { - name: i18n.RESET_CONVERSATION, - css: css` - color: ${euiThemeVars.euiColorDanger}; - `, - onClick: showDestroyModal, - icon: 'refresh', - 'data-test-subj': 'clear-chat', - }, - ], - }, - ], - [showDestroyModal] - ); - - const handleReset = useCallback(() => { - onChatCleared(); - closeDestroyModal(); - closePopover(); - }, [onChatCleared, closeDestroyModal, closePopover]); - return ( <> = ({ - - } - isOpen={isPopoverOpen} - closePopover={closePopover} - panelPaddingSize="none" - anchorPosition="downLeft" - > - - + - {isResetConversationModalVisible && ( - -

    {i18n.CLEAR_CHAT_CONFIRMATION}

    -
    - )} ); }; diff --git a/x-pack/packages/kbn-elastic-assistant/impl/assistant/assistant_header/translations.ts b/x-pack/packages/kbn-elastic-assistant/impl/assistant/assistant_header/translations.ts index 68c926d2aa14c..e4f23e0970eb0 100644 --- a/x-pack/packages/kbn-elastic-assistant/impl/assistant/assistant_header/translations.ts +++ b/x-pack/packages/kbn-elastic-assistant/impl/assistant/assistant_header/translations.ts @@ -7,6 +7,34 @@ import { i18n } from '@kbn/i18n'; +export const AI_ASSISTANT_SETTINGS = i18n.translate( + 'xpack.elasticAssistant.assistant.settings.aiAssistantSettings', + { + defaultMessage: 'AI Assistant settings', + } +); + +export const ANONYMIZATION = i18n.translate( + 'xpack.elasticAssistant.assistant.settings.anonymization', + { + defaultMessage: 'Anonymization', + } +); + +export const KNOWLEDGE_BASE = i18n.translate( + 'xpack.elasticAssistant.assistant.settings.knowledgeBase', + { + defaultMessage: 'Knowledge Base', + } +); + +export const ALERTS_TO_ANALYZE = i18n.translate( + 'xpack.elasticAssistant.assistant.settings.alertsToAnalyze', + { + defaultMessage: 'Alerts to analyze', + } +); + export const RESET_CONVERSATION = i18n.translate( 'xpack.elasticAssistant.assistant.settings.resetConversation', { diff --git a/x-pack/packages/kbn-elastic-assistant/impl/assistant/chat_send/use_chat_send.tsx b/x-pack/packages/kbn-elastic-assistant/impl/assistant/chat_send/use_chat_send.tsx index 0fabba65110b4..4ea376518b5a7 100644 --- a/x-pack/packages/kbn-elastic-assistant/impl/assistant/chat_send/use_chat_send.tsx +++ b/x-pack/packages/kbn-elastic-assistant/impl/assistant/chat_send/use_chat_send.tsx @@ -10,7 +10,6 @@ import { HttpSetup } from '@kbn/core-http-browser'; import { i18n } from '@kbn/i18n'; import { Replacements } from '@kbn/elastic-assistant-common'; import { useKnowledgeBaseStatus } from '../api/knowledge_base/use_knowledge_base_status'; -import { ESQL_RESOURCE } from '../../knowledge_base/setup_knowledge_base_button'; import { DataStreamApis } from '../use_data_stream_apis'; import { NEW_CHAT } from '../conversations/conversation_sidepanel/translations'; import type { ClientMessage } from '../../assistant_context/types'; @@ -58,12 +57,11 @@ export const useChatSend = ({ const { isLoading, sendMessage, abortStream } = useSendMessage(); const { clearConversation, removeLastMessage } = useConversation(); - const { data: kbStatus } = useKnowledgeBaseStatus({ http, resource: ESQL_RESOURCE }); + const { data: kbStatus } = useKnowledgeBaseStatus({ http }); const isSetupComplete = kbStatus?.elser_exists && kbStatus?.index_exists && kbStatus?.pipeline_exists && - kbStatus?.esql_exists && kbStatus?.security_labs_exists; // Handles sending latest user prompt to API diff --git a/x-pack/packages/kbn-elastic-assistant/impl/alerts/settings/alerts_settings.test.tsx b/x-pack/packages/kbn-elastic-assistant/impl/assistant/settings/alerts_settings/alerts_settings.test.tsx similarity index 89% rename from x-pack/packages/kbn-elastic-assistant/impl/alerts/settings/alerts_settings.test.tsx rename to x-pack/packages/kbn-elastic-assistant/impl/assistant/settings/alerts_settings/alerts_settings.test.tsx index 3e730451ba1d5..2a5cae76d5e77 100644 --- a/x-pack/packages/kbn-elastic-assistant/impl/alerts/settings/alerts_settings.test.tsx +++ b/x-pack/packages/kbn-elastic-assistant/impl/assistant/settings/alerts_settings/alerts_settings.test.tsx @@ -9,8 +9,8 @@ import { render, screen, fireEvent } from '@testing-library/react'; import React from 'react'; import { AlertsSettings } from './alerts_settings'; -import { KnowledgeBaseConfig } from '../../assistant/types'; -import { DEFAULT_LATEST_ALERTS } from '../../assistant_context/constants'; +import { KnowledgeBaseConfig } from '../../types'; +import { DEFAULT_LATEST_ALERTS } from '../../../assistant_context/constants'; describe('AlertsSettings', () => { beforeEach(() => { diff --git a/x-pack/packages/kbn-elastic-assistant/impl/alerts/settings/alerts_settings.tsx b/x-pack/packages/kbn-elastic-assistant/impl/assistant/settings/alerts_settings/alerts_settings.tsx similarity index 92% rename from x-pack/packages/kbn-elastic-assistant/impl/alerts/settings/alerts_settings.tsx rename to x-pack/packages/kbn-elastic-assistant/impl/assistant/settings/alerts_settings/alerts_settings.tsx index e73bfa15e66be..60078178a1771 100644 --- a/x-pack/packages/kbn-elastic-assistant/impl/alerts/settings/alerts_settings.tsx +++ b/x-pack/packages/kbn-elastic-assistant/impl/assistant/settings/alerts_settings/alerts_settings.tsx @@ -9,9 +9,9 @@ import { EuiFlexGroup, EuiFormRow, EuiFlexItem, EuiSpacer, EuiText } from '@elas import { css } from '@emotion/react'; import React from 'react'; -import { KnowledgeBaseConfig } from '../../assistant/types'; -import { AlertsRange } from '../../knowledge_base/alerts_range'; -import * as i18n from '../../knowledge_base/translations'; +import { KnowledgeBaseConfig } from '../../types'; +import { AlertsRange } from '../../../knowledge_base/alerts_range'; +import * as i18n from '../../../knowledge_base/translations'; export const MIN_LATEST_ALERTS = 10; export const MAX_LATEST_ALERTS = 100; diff --git a/x-pack/packages/kbn-elastic-assistant/impl/alerts/settings/alerts_settings_management.tsx b/x-pack/packages/kbn-elastic-assistant/impl/assistant/settings/alerts_settings/alerts_settings_management.tsx similarity index 68% rename from x-pack/packages/kbn-elastic-assistant/impl/alerts/settings/alerts_settings_management.tsx rename to x-pack/packages/kbn-elastic-assistant/impl/assistant/settings/alerts_settings/alerts_settings_management.tsx index d103c1a8c03c2..1a6f826bd415f 100644 --- a/x-pack/packages/kbn-elastic-assistant/impl/alerts/settings/alerts_settings_management.tsx +++ b/x-pack/packages/kbn-elastic-assistant/impl/assistant/settings/alerts_settings/alerts_settings_management.tsx @@ -7,19 +7,24 @@ import { EuiPanel, EuiSpacer, EuiText, EuiTitle } from '@elastic/eui'; import React from 'react'; -import { KnowledgeBaseConfig } from '../../assistant/types'; -import { AlertsRange } from '../../knowledge_base/alerts_range'; -import * as i18n from '../../knowledge_base/translations'; +import { KnowledgeBaseConfig } from '../../types'; +import { AlertsRange } from '../../../knowledge_base/alerts_range'; +import * as i18n from '../../../knowledge_base/translations'; interface Props { knowledgeBase: KnowledgeBaseConfig; setUpdatedKnowledgeBaseSettings: React.Dispatch>; + hasBorder?: boolean; } +/** + * Replaces the AlertsSettings component used in the existing settings modal. Once the modal is + * fully removed we can delete that component in favor of this one. + */ export const AlertsSettingsManagement: React.FC = React.memo( - ({ knowledgeBase, setUpdatedKnowledgeBaseSettings }) => { + ({ knowledgeBase, setUpdatedKnowledgeBaseSettings, hasBorder = true }) => { return ( - +

    {i18n.ALERTS_LABEL}

    diff --git a/x-pack/packages/kbn-elastic-assistant/impl/assistant/settings/assistant_settings_management.test.tsx b/x-pack/packages/kbn-elastic-assistant/impl/assistant/settings/assistant_settings_management.test.tsx index d8e207cbb23cd..dd472b3ee87ab 100644 --- a/x-pack/packages/kbn-elastic-assistant/impl/assistant/settings/assistant_settings_management.test.tsx +++ b/x-pack/packages/kbn-elastic-assistant/impl/assistant/settings/assistant_settings_management.test.tsx @@ -25,6 +25,7 @@ import { SYSTEM_PROMPTS_TAB, } from './const'; import { mockSystemPrompts } from '../../mock/system_prompt'; +import { DataViewsContract } from '@kbn/data-views-plugin/public'; const mockConversations = { [alertConvo.title]: alertConvo, @@ -53,8 +54,13 @@ const mockContext = { }, }; +const mockDataViews = { + getIndices: jest.fn(), +} as unknown as DataViewsContract; + const testProps = { selectedConversation: welcomeConvo, + dataViews: mockDataViews, }; jest.mock('../../assistant_context'); diff --git a/x-pack/packages/kbn-elastic-assistant/impl/assistant/settings/assistant_settings_management.tsx b/x-pack/packages/kbn-elastic-assistant/impl/assistant/settings/assistant_settings_management.tsx index 89c00fbf88773..4c50d14a5662e 100644 --- a/x-pack/packages/kbn-elastic-assistant/impl/assistant/settings/assistant_settings_management.tsx +++ b/x-pack/packages/kbn-elastic-assistant/impl/assistant/settings/assistant_settings_management.tsx @@ -9,6 +9,7 @@ import React, { useEffect, useMemo } from 'react'; import { EuiAvatar, EuiPageTemplate, EuiTitle, useEuiShadow, useEuiTheme } from '@elastic/eui'; import { css } from '@emotion/react'; +import { DataViewsContract } from '@kbn/data-views-plugin/public'; import { Conversation } from '../../..'; import * as i18n from './translations'; import { useAssistantContext } from '../../assistant_context'; @@ -33,6 +34,7 @@ import { KnowledgeBaseSettingsManagement } from '../../knowledge_base/knowledge_ import { EvaluationSettings } from '.'; interface Props { + dataViews: DataViewsContract; selectedConversation: Conversation; } @@ -41,7 +43,7 @@ interface Props { * anonymization, knowledge base, and evaluation via the `isModelEvaluationEnabled` feature flag. */ export const AssistantSettingsManagement: React.FC = React.memo( - ({ selectedConversation: defaultSelectedConversation }) => { + ({ dataViews, selectedConversation: defaultSelectedConversation }) => { const { assistantFeatures: { assistantModelEvaluation: modelEvaluatorEnabled }, http, @@ -158,7 +160,9 @@ export const AssistantSettingsManagement: React.FC = React.memo( )} {selectedSettingsTab === QUICK_PROMPTS_TAB && } {selectedSettingsTab === ANONYMIZATION_TAB && } - {selectedSettingsTab === KNOWLEDGE_BASE_TAB && } + {selectedSettingsTab === KNOWLEDGE_BASE_TAB && ( + + )} {selectedSettingsTab === EVALUATION_TAB && } diff --git a/x-pack/packages/kbn-elastic-assistant/impl/assistant/settings/settings_context_menu/settings_context_menu.tsx b/x-pack/packages/kbn-elastic-assistant/impl/assistant/settings/settings_context_menu/settings_context_menu.tsx new file mode 100644 index 0000000000000..b7f33b9a6af5a --- /dev/null +++ b/x-pack/packages/kbn-elastic-assistant/impl/assistant/settings/settings_context_menu/settings_context_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; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React, { ReactElement, useCallback, useMemo, useState } from 'react'; +import { + EuiFlexGroup, + EuiFlexItem, + EuiContextMenuPanel, + EuiContextMenuItem, + EuiConfirmModal, + EuiNotificationBadge, + EuiPopover, + EuiButtonIcon, +} from '@elastic/eui'; +import { css } from '@emotion/react'; +import { euiThemeVars } from '@kbn/ui-theme'; +import { useAssistantContext } from '../../../..'; +import * as i18n from '../../assistant_header/translations'; + +interface Params { + isDisabled?: boolean; + onChatCleared?: () => void; +} + +export const SettingsContextMenu: React.FC = React.memo( + ({ isDisabled = false, onChatCleared }: Params) => { + const { + navigateToApp, + knowledgeBase, + assistantFeatures: { assistantKnowledgeBaseByDefault: enableKnowledgeBaseByDefault }, + } = useAssistantContext(); + + const [isPopoverOpen, setPopover] = useState(false); + + const [isResetConversationModalVisible, setIsResetConversationModalVisible] = useState(false); + const closeDestroyModal = useCallback(() => setIsResetConversationModalVisible(false), []); + + const onButtonClick = useCallback(() => { + setPopover(!isPopoverOpen); + }, [isPopoverOpen]); + + const closePopover = useCallback(() => { + setPopover(false); + }, []); + + const showDestroyModal = useCallback(() => { + closePopover?.(); + setIsResetConversationModalVisible(true); + }, [closePopover]); + + const handleNavigateToSettings = useCallback( + () => + navigateToApp('management', { + path: 'kibana/securityAiAssistantManagement', + }), + [navigateToApp] + ); + + const handleNavigateToKnowledgeBase = useCallback( + () => + navigateToApp('management', { + path: 'kibana/securityAiAssistantManagement', + }), + [navigateToApp] + ); + + // We are migrating away from the settings modal in favor of the new Stack Management UI + // Currently behind `assistantKnowledgeBaseByDefault` FF + const newItems: ReactElement[] = useMemo( + () => [ + + {i18n.AI_ASSISTANT_SETTINGS} + , + + {i18n.ANONYMIZATION} + , + + {i18n.KNOWLEDGE_BASE} + , + + + {i18n.ALERTS_TO_ANALYZE} + + + {knowledgeBase.latestAlerts} + + + + , + ], + [handleNavigateToKnowledgeBase, handleNavigateToSettings, knowledgeBase] + ); + + const items = useMemo( + () => [ + ...(enableKnowledgeBaseByDefault ? newItems : []), + + {i18n.RESET_CONVERSATION} + , + ], + + [enableKnowledgeBaseByDefault, newItems, showDestroyModal] + ); + + const handleReset = useCallback(() => { + onChatCleared?.(); + closeDestroyModal(); + closePopover?.(); + }, [onChatCleared, closeDestroyModal, closePopover]); + + return ( + <> + + } + isOpen={isPopoverOpen} + closePopover={closePopover} + panelPaddingSize="none" + anchorPosition="leftUp" + > + + + {isResetConversationModalVisible && ( + +

    {i18n.CLEAR_CHAT_CONFIRMATION}

    +
    + )} + + ); + } +); + +SettingsContextMenu.displayName = 'SettingsContextMenu'; diff --git a/x-pack/packages/kbn-elastic-assistant/impl/connectorland/helpers.tsx b/x-pack/packages/kbn-elastic-assistant/impl/connectorland/helpers.tsx index 2bbc74af5a45a..99550f1cafe75 100644 --- a/x-pack/packages/kbn-elastic-assistant/impl/connectorland/helpers.tsx +++ b/x-pack/packages/kbn-elastic-assistant/impl/connectorland/helpers.tsx @@ -18,6 +18,7 @@ import { PRECONFIGURED_CONNECTOR } from './translations'; enum OpenAiProviderType { OpenAi = 'OpenAI', AzureAi = 'Azure OpenAI', + Other = 'Other', } interface GenAiConfig { diff --git a/x-pack/packages/kbn-elastic-assistant/impl/knowledge_base/alerts_range.tsx b/x-pack/packages/kbn-elastic-assistant/impl/knowledge_base/alerts_range.tsx index 152f0a91a7d04..63bd86121dcc1 100644 --- a/x-pack/packages/kbn-elastic-assistant/impl/knowledge_base/alerts_range.tsx +++ b/x-pack/packages/kbn-elastic-assistant/impl/knowledge_base/alerts_range.tsx @@ -12,7 +12,7 @@ import { MAX_LATEST_ALERTS, MIN_LATEST_ALERTS, TICK_INTERVAL, -} from '../alerts/settings/alerts_settings'; +} from '../assistant/settings/alerts_settings/alerts_settings'; import { KnowledgeBaseConfig } from '../assistant/types'; import { ALERTS_RANGE } from './translations'; diff --git a/x-pack/packages/kbn-elastic-assistant/impl/knowledge_base/knowledge_base_settings.test.tsx b/x-pack/packages/kbn-elastic-assistant/impl/knowledge_base/knowledge_base_settings.test.tsx index 67b48ac9354d7..3d18885902326 100644 --- a/x-pack/packages/kbn-elastic-assistant/impl/knowledge_base/knowledge_base_settings.test.tsx +++ b/x-pack/packages/kbn-elastic-assistant/impl/knowledge_base/knowledge_base_settings.test.tsx @@ -69,7 +69,6 @@ jest.mock('../assistant/api/knowledge_base/use_knowledge_base_status', () => ({ return { data: { elser_exists: true, - esql_exists: true, index_exists: true, pipeline_exists: true, }, @@ -83,22 +82,11 @@ describe('Knowledge base settings', () => { beforeEach(() => { jest.clearAllMocks(); }); - it('Shows correct description when esql is installed', () => { - const { getByTestId, queryByTestId } = render( - - - - ); - - expect(getByTestId('esql-installed')).toBeInTheDocument(); - expect(queryByTestId('install-esql')).not.toBeInTheDocument(); - }); it('On enable knowledge base, call setup knowledge base setup', () => { (useKnowledgeBaseStatus as jest.Mock).mockImplementation(() => { return { data: { elser_exists: true, - esql_exists: false, index_exists: false, pipeline_exists: false, is_setup_available: true, @@ -115,14 +103,13 @@ describe('Knowledge base settings', () => { expect(queryByTestId('kb-installed')).not.toBeInTheDocument(); expect(getByTestId('install-kb')).toBeInTheDocument(); fireEvent.click(getByTestId('setupKnowledgeBaseButton')); - expect(mockSetup).toHaveBeenCalledWith('esql'); + expect(mockSetup).toHaveBeenCalled(); }); it('If elser does not exist, do not offer knowledge base', () => { (useKnowledgeBaseStatus as jest.Mock).mockImplementation(() => { return { data: { elser_exists: false, - esql_exists: false, index_exists: false, pipeline_exists: false, }, diff --git a/x-pack/packages/kbn-elastic-assistant/impl/knowledge_base/knowledge_base_settings.tsx b/x-pack/packages/kbn-elastic-assistant/impl/knowledge_base/knowledge_base_settings.tsx index df254805d9cee..aa873decdcd87 100644 --- a/x-pack/packages/kbn-elastic-assistant/impl/knowledge_base/knowledge_base_settings.tsx +++ b/x-pack/packages/kbn-elastic-assistant/impl/knowledge_base/knowledge_base_settings.tsx @@ -23,7 +23,7 @@ import { import { FormattedMessage } from '@kbn/i18n-react'; import { css } from '@emotion/react'; -import { AlertsSettings } from '../alerts/settings/alerts_settings'; +import { AlertsSettings } from '../assistant/settings/alerts_settings/alerts_settings'; import { useAssistantContext } from '../assistant_context'; import type { KnowledgeBaseConfig } from '../assistant/types'; import * as i18n from './translations'; @@ -31,7 +31,6 @@ import { useKnowledgeBaseStatus } from '../assistant/api/knowledge_base/use_know import { useSetupKnowledgeBase } from '../assistant/api/knowledge_base/use_setup_knowledge_base'; import { SETUP_KNOWLEDGE_BASE_BUTTON_TOOLTIP } from './translations'; -const ESQL_RESOURCE = 'esql'; const KNOWLEDGE_BASE_INDEX_PATTERN = '.kibana-elastic-ai-assistant-knowledge-base-(SPACE)'; interface Props { @@ -45,20 +44,14 @@ interface Props { export const KnowledgeBaseSettings: React.FC = React.memo( ({ knowledgeBase, setUpdatedKnowledgeBaseSettings }) => { const { http, toasts } = useAssistantContext(); - const { - data: kbStatus, - isLoading, - isFetching, - } = useKnowledgeBaseStatus({ http, resource: ESQL_RESOURCE }); + const { data: kbStatus, isLoading, isFetching } = useKnowledgeBaseStatus({ http }); const { mutate: setupKB, isLoading: isSettingUpKB } = useSetupKnowledgeBase({ http, toasts }); // Resource enabled state const isElserEnabled = kbStatus?.elser_exists ?? false; - const isESQLEnabled = kbStatus?.esql_exists ?? false; const isSecurityLabsEnabled = kbStatus?.security_labs_exists ?? false; const isKnowledgeBaseSetup = (isElserEnabled && - isESQLEnabled && isSecurityLabsEnabled && kbStatus?.index_exists && kbStatus?.pipeline_exists) ?? @@ -72,12 +65,11 @@ export const KnowledgeBaseSettings: React.FC = React.memo( // Calculated health state for EuiHealth component const elserHealth = isElserEnabled ? 'success' : 'subdued'; const knowledgeBaseHealth = isKnowledgeBaseSetup ? 'success' : 'subdued'; - const esqlHealth = isESQLEnabled ? 'success' : 'subdued'; ////////////////////////////////////////////////////////////////////////////////////////// // Main `Knowledge Base` setup button const onSetupKnowledgeBaseButtonClick = useCallback(() => { - setupKB(ESQL_RESOURCE); + setupKB(); }, [setupKB]); const toolTipContent = !isSetupAvailable ? SETUP_KNOWLEDGE_BASE_BUTTON_TOOLTIP : undefined; @@ -119,16 +111,6 @@ export const KnowledgeBaseSettings: React.FC = React.memo( ); }, [isKnowledgeBaseSetup]); - ////////////////////////////////////////////////////////////////////////////////////////// - // ESQL Resource - const esqlDescription = useMemo(() => { - return isESQLEnabled ? ( - {i18n.ESQL_DESCRIPTION_INSTALLED} - ) : ( - {i18n.ESQL_DESCRIPTION} - ); - }, [isESQLEnabled]); - return ( <> @@ -208,20 +190,6 @@ export const KnowledgeBaseSettings: React.FC = React.memo(
    - - - {i18n.ESQL_LABEL} - - {esqlDescription} - - - diff --git a/x-pack/packages/kbn-elastic-assistant/impl/knowledge_base/knowledge_base_settings_management/document_entry_editor.tsx b/x-pack/packages/kbn-elastic-assistant/impl/knowledge_base/knowledge_base_settings_management/document_entry_editor.tsx index 016da27d2c051..b33f221bfde3b 100644 --- a/x-pack/packages/kbn-elastic-assistant/impl/knowledge_base/knowledge_base_settings_management/document_entry_editor.tsx +++ b/x-pack/packages/kbn-elastic-assistant/impl/knowledge_base/knowledge_base_settings_management/document_entry_editor.tsx @@ -127,7 +127,6 @@ export const DocumentEntryEditor: React.FC = React.memo(({ entry, setEntr id="requiredKnowledge" onChange={onRequiredKnowledgeChanged} checked={entry?.required ?? false} - disabled={true} /> diff --git a/x-pack/packages/kbn-elastic-assistant/impl/knowledge_base/knowledge_base_settings_management/index.tsx b/x-pack/packages/kbn-elastic-assistant/impl/knowledge_base/knowledge_base_settings_management/index.tsx index a2097177a2ca4..5cf887ae3375d 100644 --- a/x-pack/packages/kbn-elastic-assistant/impl/knowledge_base/knowledge_base_settings_management/index.tsx +++ b/x-pack/packages/kbn-elastic-assistant/impl/knowledge_base/knowledge_base_settings_management/index.tsx @@ -6,8 +6,12 @@ */ import { + EuiButton, + EuiFlexGroup, + EuiFlexItem, EuiInMemoryTable, EuiLink, + EuiLoadingSpinner, EuiPanel, EuiSearchBarProps, EuiSpacer, @@ -23,7 +27,9 @@ import { KnowledgeBaseEntryCreateProps, KnowledgeBaseEntryResponse, } from '@kbn/elastic-assistant-common'; -import { AlertsSettingsManagement } from '../../alerts/settings/alerts_settings_management'; +import { css } from '@emotion/react'; +import { DataViewsContract } from '@kbn/data-views-plugin/public'; +import { AlertsSettingsManagement } from '../../assistant/settings/alerts_settings/alerts_settings_management'; import { useKnowledgeBaseEntries } from '../../assistant/api/knowledge_base/entries/use_knowledge_base_entries'; import { useAssistantContext } from '../../assistant_context'; import { useKnowledgeBaseTable } from './use_knowledge_base_table'; @@ -51,14 +57,24 @@ import { useCreateKnowledgeBaseEntry } from '../../assistant/api/knowledge_base/ import { useUpdateKnowledgeBaseEntries } from '../../assistant/api/knowledge_base/entries/use_update_knowledge_base_entries'; import { SETTINGS_UPDATED_TOAST_TITLE } from '../../assistant/settings/translations'; import { KnowledgeBaseConfig } from '../../assistant/types'; +import { + isKnowledgeBaseSetup, + useKnowledgeBaseStatus, +} from '../../assistant/api/knowledge_base/use_knowledge_base_status'; + +interface Params { + dataViews: DataViewsContract; +} -export const KnowledgeBaseSettingsManagement: React.FC = React.memo(() => { +export const KnowledgeBaseSettingsManagement: React.FC = React.memo(({ dataViews }) => { const { assistantFeatures: { assistantKnowledgeBaseByDefault: enableKnowledgeBaseByDefault }, http, toasts, } = useAssistantContext(); const [hasPendingChanges, setHasPendingChanges] = useState(false); + const { data: kbStatus, isFetched } = useKnowledgeBaseStatus({ http }); + const isKbSetup = isKnowledgeBaseSetup(kbStatus); // Only needed for legacy settings management const { knowledgeBase, setUpdatedKnowledgeBaseSettings, resetSettings, saveSettings } = @@ -123,12 +139,12 @@ export const KnowledgeBaseSettingsManagement: React.FC = React.memo(() => { // Flyout Save/Cancel Actions const onSaveConfirmed = useCallback(() => { - if (isKnowledgeBaseEntryCreateProps(selectedEntry)) { - createEntry(selectedEntry); - closeFlyout(); - } else if (isKnowledgeBaseEntryResponse(selectedEntry)) { + if (isKnowledgeBaseEntryResponse(selectedEntry)) { updateEntries([selectedEntry]); closeFlyout(); + } else if (isKnowledgeBaseEntryCreateProps(selectedEntry)) { + createEntry(selectedEntry); + closeFlyout(); } }, [closeFlyout, selectedEntry, createEntry, updateEntries]); @@ -137,7 +153,11 @@ export const KnowledgeBaseSettingsManagement: React.FC = React.memo(() => { closeFlyout(); }, [closeFlyout]); - const { data: entries } = useKnowledgeBaseEntries({ + const { + data: entries, + isFetching: isFetchingEntries, + refetch: refetchEntries, + } = useKnowledgeBaseEntries({ http, toasts, enabled: enableKnowledgeBaseByDefault, @@ -169,6 +189,9 @@ export const KnowledgeBaseSettingsManagement: React.FC = React.memo(() => { [deleteEntry, entries.data, getColumns, openFlyout] ); + // Refresh button + const handleRefreshTable = useCallback(() => refetchEntries(), [refetchEntries]); + const onDocumentClicked = useCallback(() => { setSelectedEntry({ type: DocumentEntryType.value, kbResource: 'user', source: 'user' }); openFlyout(); @@ -182,7 +205,30 @@ export const KnowledgeBaseSettingsManagement: React.FC = React.memo(() => { const search: EuiSearchBarProps = useMemo( () => ({ toolsRight: ( - + + + + + + + + + + ), box: { incremental: true, @@ -190,7 +236,7 @@ export const KnowledgeBaseSettingsManagement: React.FC = React.memo(() => { }, filters: [], }), - [onDocumentClicked, onIndexClicked] + [isFetchingEntries, handleRefreshTable, onDocumentClicked, onIndexClicked] ); const flyoutTitle = useMemo(() => { @@ -247,15 +293,40 @@ export const KnowledgeBaseSettingsManagement: React.FC = React.memo(() => { ), }} /> -
    - + + + {!isFetched ? ( + + ) : isKbSetup ? ( + + ) : ( + <> + + + + + + + + + + + + + + )} + +
    { ) : ( >> } diff --git a/x-pack/packages/kbn-elastic-assistant/impl/knowledge_base/knowledge_base_settings_management/index_entry_editor.tsx b/x-pack/packages/kbn-elastic-assistant/impl/knowledge_base/knowledge_base_settings_management/index_entry_editor.tsx index 19f8cfbbc52ba..f5dd2df3bcaac 100644 --- a/x-pack/packages/kbn-elastic-assistant/impl/knowledge_base/knowledge_base_settings_management/index_entry_editor.tsx +++ b/x-pack/packages/kbn-elastic-assistant/impl/knowledge_base/knowledge_base_settings_management/index_entry_editor.tsx @@ -17,14 +17,16 @@ import { } from '@elastic/eui'; import React, { useCallback } from 'react'; import { IndexEntry } from '@kbn/elastic-assistant-common'; +import { DataViewsContract } from '@kbn/data-views-plugin/public'; import * as i18n from './translations'; interface Props { + dataViews: DataViewsContract; entry?: IndexEntry; setEntry: React.Dispatch>>; } -export const IndexEntryEditor: React.FC = React.memo(({ entry, setEntry }) => { +export const IndexEntryEditor: React.FC = React.memo(({ dataViews, entry, setEntry }) => { // Name const setName = useCallback( (e: React.ChangeEvent) => @@ -74,9 +76,17 @@ export const IndexEntryEditor: React.FC = React.memo(({ entry, setEntry } entry?.users?.length === 0 ? sharingOptions[1].value : sharingOptions[0].value; // Index + // TODO: For index field autocomplete + // const indexOptions = useMemo(() => { + // const indices = await dataViews.getIndices({ + // pattern: e[0]?.value ?? '', + // isRollupIndex: () => false, + // }); + // }, [dataViews]); const setIndex = useCallback( - (e: Array>) => - setEntry((prevEntry) => ({ ...prevEntry, index: e[0].value })), + async (e: Array>) => { + setEntry((prevEntry) => ({ ...prevEntry, index: e[0]?.value })); + }, [setEntry] ); @@ -162,30 +172,51 @@ export const IndexEntryEditor: React.FC = React.memo(({ entry, setEntry } - + - + + + + ); }); diff --git a/x-pack/packages/kbn-elastic-assistant/impl/knowledge_base/knowledge_base_settings_management/translations.ts b/x-pack/packages/kbn-elastic-assistant/impl/knowledge_base/knowledge_base_settings_management/translations.ts index ed4a3676975b8..0cc16089fdaae 100644 --- a/x-pack/packages/kbn-elastic-assistant/impl/knowledge_base/knowledge_base_settings_management/translations.ts +++ b/x-pack/packages/kbn-elastic-assistant/impl/knowledge_base/knowledge_base_settings_management/translations.ts @@ -251,14 +251,44 @@ export const ENTRY_FIELD_INPUT_LABEL = i18n.translate( export const ENTRY_DESCRIPTION_INPUT_LABEL = i18n.translate( 'xpack.elasticAssistant.assistant.settings.knowledgeBaseSettingsManagement.entryDescriptionInputLabel', { - defaultMessage: 'Description', + defaultMessage: 'Data Description', + } +); + +export const ENTRY_DESCRIPTION_HELP_LABEL = i18n.translate( + 'xpack.elasticAssistant.assistant.settings.knowledgeBaseSettingsManagement.entryDescriptionHelpLabel', + { + defaultMessage: + 'A description of the type of data in this index and/or when the assistant should look for data here.', } ); export const ENTRY_QUERY_DESCRIPTION_INPUT_LABEL = i18n.translate( 'xpack.elasticAssistant.assistant.settings.knowledgeBaseSettingsManagement.entryQueryDescriptionInputLabel', { - defaultMessage: 'Query Description', + defaultMessage: 'Query Instruction', + } +); + +export const ENTRY_QUERY_DESCRIPTION_HELP_LABEL = i18n.translate( + 'xpack.elasticAssistant.assistant.settings.knowledgeBaseSettingsManagement.entryQueryDescriptionHelpLabel', + { + defaultMessage: 'Any instructions for extracting the search query from the user request.', + } +); + +export const ENTRY_OUTPUT_FIELDS_INPUT_LABEL = i18n.translate( + 'xpack.elasticAssistant.assistant.settings.knowledgeBaseSettingsManagement.entryOutputFieldsInputLabel', + { + defaultMessage: 'Output Fields', + } +); + +export const ENTRY_OUTPUT_FIELDS_HELP_LABEL = i18n.translate( + 'xpack.elasticAssistant.assistant.settings.knowledgeBaseSettingsManagement.entryOutputFieldsHelpLabel', + { + defaultMessage: + 'What fields should be sent to the LLM. Leave empty to send the entire document.', } ); @@ -269,6 +299,13 @@ export const ENTRY_INPUT_PLACEHOLDER = i18n.translate( } ); +export const ENTRY_FIELD_PLACEHOLDER = i18n.translate( + 'xpack.elasticAssistant.assistant.settings.knowledgeBaseSettingsManagement.entryFieldPlaceholder', + { + defaultMessage: 'semantic_text', + } +); + export const KNOWLEDGE_BASE_DOCUMENTATION = i18n.translate( 'xpack.elasticAssistant.assistant.settings.knowledgeBaseSettingsManagement.knowledgeBaseDocumentation', { diff --git a/x-pack/packages/kbn-elastic-assistant/impl/knowledge_base/knowledge_base_settings_management/use_knowledge_base_table.tsx b/x-pack/packages/kbn-elastic-assistant/impl/knowledge_base/knowledge_base_settings_management/use_knowledge_base_table.tsx index 5af360a598205..d0038169cd597 100644 --- a/x-pack/packages/kbn-elastic-assistant/impl/knowledge_base/knowledge_base_settings_management/use_knowledge_base_table.tsx +++ b/x-pack/packages/kbn-elastic-assistant/impl/knowledge_base/knowledge_base_settings_management/use_knowledge_base_table.tsx @@ -5,7 +5,7 @@ * 2.0. */ -import { EuiAvatar, EuiBadge, EuiBasicTableColumn, EuiIcon, EuiLink, EuiText } from '@elastic/eui'; +import { EuiAvatar, EuiBadge, EuiBasicTableColumn, EuiIcon, EuiText } from '@elastic/eui'; import { css } from '@emotion/react'; import React, { useCallback } from 'react'; import { FormattedDate } from '@kbn/i18n-react'; @@ -32,7 +32,7 @@ export const useKnowledgeBaseTable = () => { if (['esql', 'security_labs'].includes(entry.kbResource)) { return 'logoElastic'; } - return 'visText'; + return 'document'; } else if (entry.type === IndexEntryType.value) { return 'index'; } @@ -61,9 +61,7 @@ export const useKnowledgeBaseTable = () => { }, { name: i18n.COLUMN_NAME, - render: (entry: KnowledgeBaseEntryResponse) => ( - onEntryNameClicked(entry)}>{entry.name} - ), + render: ({ name }: KnowledgeBaseEntryResponse) => name, sortable: ({ name }: KnowledgeBaseEntryResponse) => name, width: '30%', }, diff --git a/x-pack/packages/kbn-elastic-assistant/impl/knowledge_base/setup_knowledge_base_button.tsx b/x-pack/packages/kbn-elastic-assistant/impl/knowledge_base/setup_knowledge_base_button.tsx index 533f3fe35922c..d697fc7120d01 100644 --- a/x-pack/packages/kbn-elastic-assistant/impl/knowledge_base/setup_knowledge_base_button.tsx +++ b/x-pack/packages/kbn-elastic-assistant/impl/knowledge_base/setup_knowledge_base_button.tsx @@ -13,8 +13,6 @@ import { useAssistantContext } from '../..'; import { useSetupKnowledgeBase } from '../assistant/api/knowledge_base/use_setup_knowledge_base'; import { useKnowledgeBaseStatus } from '../assistant/api/knowledge_base/use_knowledge_base_status'; -export const ESQL_RESOURCE = 'esql'; - interface Props { display?: 'mini'; } @@ -26,7 +24,7 @@ interface Props { export const SetupKnowledgeBaseButton: React.FC = React.memo(({ display }: Props) => { const { http, toasts } = useAssistantContext(); - const { data: kbStatus } = useKnowledgeBaseStatus({ http, resource: ESQL_RESOURCE }); + const { data: kbStatus } = useKnowledgeBaseStatus({ http }); const { mutate: setupKB, isLoading: isSettingUpKB } = useSetupKnowledgeBase({ http, toasts }); const isSetupInProgress = kbStatus?.is_setup_in_progress || isSettingUpKB; @@ -34,11 +32,10 @@ export const SetupKnowledgeBaseButton: React.FC = React.memo(({ display } kbStatus?.elser_exists && kbStatus?.index_exists && kbStatus?.pipeline_exists && - kbStatus?.esql_exists && kbStatus?.security_labs_exists; const onInstallKnowledgeBase = useCallback(() => { - setupKB(ESQL_RESOURCE); + setupKB(); }, [setupKB]); if (isSetupComplete) { diff --git a/x-pack/packages/kbn-elastic-assistant/tsconfig.json b/x-pack/packages/kbn-elastic-assistant/tsconfig.json index ed2631b597bd6..8d19fa86f4d11 100644 --- a/x-pack/packages/kbn-elastic-assistant/tsconfig.json +++ b/x-pack/packages/kbn-elastic-assistant/tsconfig.json @@ -30,5 +30,6 @@ "@kbn/core-doc-links-browser", "@kbn/core", "@kbn/zod", + "@kbn/data-views-plugin", ] } diff --git a/x-pack/packages/kbn-entities-schema/src/schema/__snapshots__/common.test.ts.snap b/x-pack/packages/kbn-entities-schema/src/schema/__snapshots__/common.test.ts.snap index 9210d3b9991cf..766ce1c70ac3a 100644 --- a/x-pack/packages/kbn-entities-schema/src/schema/__snapshots__/common.test.ts.snap +++ b/x-pack/packages/kbn-entities-schema/src/schema/__snapshots__/common.test.ts.snap @@ -78,7 +78,8 @@ exports[`schemas metadataSchema should parse successfully with a source and desi Object { "data": Object { "aggregation": Object { - "limit": 1000, + "limit": 10, + "lookbackPeriod": undefined, "type": "terms", }, "destination": "hostName", @@ -92,7 +93,8 @@ exports[`schemas metadataSchema should parse successfully with an valid string 1 Object { "data": Object { "aggregation": Object { - "limit": 1000, + "limit": 10, + "lookbackPeriod": undefined, "type": "terms", }, "destination": "host.name", @@ -106,7 +108,8 @@ exports[`schemas metadataSchema should parse successfully with just a source 1`] Object { "data": Object { "aggregation": Object { - "limit": 1000, + "limit": 10, + "lookbackPeriod": undefined, "type": "terms", }, "destination": "host.name", @@ -120,7 +123,8 @@ exports[`schemas metadataSchema should parse successfully with valid object 1`] Object { "data": Object { "aggregation": Object { - "limit": 1000, + "limit": 10, + "lookbackPeriod": undefined, "type": "terms", }, "destination": "hostName", diff --git a/x-pack/packages/kbn-entities-schema/src/schema/common.test.ts b/x-pack/packages/kbn-entities-schema/src/schema/common.test.ts index 1a737ac3f4d9b..210e34943bd40 100644 --- a/x-pack/packages/kbn-entities-schema/src/schema/common.test.ts +++ b/x-pack/packages/kbn-entities-schema/src/schema/common.test.ts @@ -5,7 +5,7 @@ * 2.0. */ -import { durationSchema, metadataSchema, semVerSchema, historySettingsSchema } from './common'; +import { durationSchema, metadataSchema, semVerSchema } from './common'; describe('schemas', () => { describe('metadataSchema', () => { @@ -66,7 +66,7 @@ describe('schemas', () => { expect(result.data).toEqual({ source: 'host.name', destination: 'hostName', - aggregation: { type: 'terms', limit: 1000 }, + aggregation: { type: 'terms', limit: 10, lookbackPeriod: undefined }, }); }); @@ -139,30 +139,4 @@ describe('schemas', () => { expect(result).toMatchSnapshot(); }); }); - - describe('historySettingsSchema', () => { - it('should return default values when not defined', () => { - let result = historySettingsSchema.safeParse(undefined); - expect(result.success).toBeTruthy(); - expect(result.data).toEqual({ lookbackPeriod: '1h' }); - - result = historySettingsSchema.safeParse({ syncDelay: '1m' }); - expect(result.success).toBeTruthy(); - expect(result.data).toEqual({ syncDelay: '1m', lookbackPeriod: '1h' }); - }); - - it('should return user defined values when defined', () => { - const result = historySettingsSchema.safeParse({ - lookbackPeriod: '30m', - syncField: 'event.ingested', - syncDelay: '5m', - }); - expect(result.success).toBeTruthy(); - expect(result.data).toEqual({ - lookbackPeriod: '30m', - syncField: 'event.ingested', - syncDelay: '5m', - }); - }); - }); }); diff --git a/x-pack/packages/kbn-entities-schema/src/schema/common.ts b/x-pack/packages/kbn-entities-schema/src/schema/common.ts index aa54dbd16c9aa..caecf48d88aac 100644 --- a/x-pack/packages/kbn-entities-schema/src/schema/common.ts +++ b/x-pack/packages/kbn-entities-schema/src/schema/common.ts @@ -85,7 +85,11 @@ export const keyMetricSchema = z.object({ export type KeyMetric = z.infer; export const metadataAggregation = z.union([ - z.object({ type: z.literal('terms'), limit: z.number().default(1000) }), + z.object({ + type: z.literal('terms'), + limit: z.number().default(10), + lookbackPeriod: z.optional(durationSchema), + }), z.object({ type: z.literal('top_value'), sort: z.record(z.string(), z.union([z.literal('asc'), z.literal('desc')])), @@ -99,13 +103,13 @@ export const metadataSchema = z destination: z.optional(z.string()), aggregation: z .optional(metadataAggregation) - .default({ type: z.literal('terms').value, limit: 1000 }), + .default({ type: z.literal('terms').value, limit: 10, lookbackPeriod: undefined }), }) .or( z.string().transform((value) => ({ source: value, destination: value, - aggregation: { type: z.literal('terms').value, limit: 1000 }, + aggregation: { type: z.literal('terms').value, limit: 10, lookbackPeriod: undefined }, })) ) .transform((metadata) => ({ diff --git a/x-pack/packages/kbn-entities-schema/src/schema/entity.ts b/x-pack/packages/kbn-entities-schema/src/schema/entity.ts index eae6873356c14..3eb87a797ef21 100644 --- a/x-pack/packages/kbn-entities-schema/src/schema/entity.ts +++ b/x-pack/packages/kbn-entities-schema/src/schema/entity.ts @@ -35,7 +35,6 @@ export const entityLatestSchema = z entity: entityBaseSchema.merge( z.object({ lastSeenTimestamp: z.string(), - firstSeenTimestamp: z.string(), }) ), }) diff --git a/x-pack/packages/kbn-entities-schema/src/schema/entity_definition.ts b/x-pack/packages/kbn-entities-schema/src/schema/entity_definition.ts index 74be36cc5d802..d9d8e6b610013 100644 --- a/x-pack/packages/kbn-entities-schema/src/schema/entity_definition.ts +++ b/x-pack/packages/kbn-entities-schema/src/schema/entity_definition.ts @@ -14,8 +14,6 @@ import { durationSchema, identityFieldsSchema, semVerSchema, - historySettingsSchema, - durationSchemaWithMinimum, } from './common'; export const entityDefinitionSchema = z.object({ @@ -32,22 +30,17 @@ export const entityDefinitionSchema = z.object({ metrics: z.optional(z.array(keyMetricSchema)), staticFields: z.optional(z.record(z.string(), z.string())), managed: z.optional(z.boolean()).default(false), - history: z.object({ + latest: z.object({ timestampField: z.string(), - interval: durationSchemaWithMinimum(1), - settings: historySettingsSchema, + lookbackPeriod: z.optional(durationSchema).default('24h'), + settings: z.optional( + z.object({ + syncField: z.optional(z.string()), + syncDelay: z.optional(durationSchema), + frequency: z.optional(durationSchema), + }) + ), }), - latest: z.optional( - z.object({ - settings: z.optional( - z.object({ - syncField: z.optional(z.string()), - syncDelay: z.optional(durationSchema), - frequency: z.optional(durationSchema), - }) - ), - }) - ), installStatus: z.optional( z.union([ z.literal('installing'), @@ -57,6 +50,18 @@ export const entityDefinitionSchema = z.object({ ]) ), installStartedAt: z.optional(z.string()), + installedComponents: z.optional( + z.array( + z.object({ + type: z.union([ + z.literal('transform'), + z.literal('ingest_pipeline'), + z.literal('template'), + ]), + id: z.string(), + }) + ) + ), }); export const entityDefinitionUpdateSchema = entityDefinitionSchema @@ -69,7 +74,7 @@ export const entityDefinitionUpdateSchema = entityDefinitionSchema .partial() .merge( z.object({ - history: z.optional(entityDefinitionSchema.shape.history.partial()), + latest: z.optional(entityDefinitionSchema.shape.latest.partial()), version: semVerSchema, }) ); diff --git a/x-pack/packages/kbn-langchain/server/language_models/chat_vertex/chat_vertex.test.ts b/x-pack/packages/kbn-langchain/server/language_models/chat_vertex/chat_vertex.test.ts index 37506922ff69b..07fe252bd5074 100644 --- a/x-pack/packages/kbn-langchain/server/language_models/chat_vertex/chat_vertex.test.ts +++ b/x-pack/packages/kbn-langchain/server/language_models/chat_vertex/chat_vertex.test.ts @@ -12,6 +12,7 @@ import { actionsClientMock } from '@kbn/actions-plugin/server/actions_client/act import { BaseMessage, HumanMessage, SystemMessage } from '@langchain/core/messages'; import { ActionsClientChatVertexAI } from './chat_vertex'; import { CallbackManagerForLLMRun } from '@langchain/core/callbacks/manager'; +import { GeminiContent } from '@langchain/google-common'; const connectorId = 'mock-connector-id'; @@ -54,8 +55,10 @@ const mockStreamExecute = jest.fn().mockImplementation(() => { }; }); +const systemInstruction = 'Answer the following questions truthfully and as best you can.'; + const callMessages = [ - new SystemMessage('Answer the following questions truthfully and as best you can.'), + new SystemMessage(systemInstruction), new HumanMessage('Question: Do you know my name?\n\n'), ] as unknown as BaseMessage[]; @@ -196,4 +199,32 @@ describe('ActionsClientChatVertexAI', () => { expect(handleLLMNewToken).toHaveBeenCalledWith('token3'); }); }); + + describe('message formatting', () => { + it('Properly sorts out the system role', async () => { + const actionsClientChatVertexAI = new ActionsClientChatVertexAI(defaultArgs); + + await actionsClientChatVertexAI._generate(callMessages, callOptions, callRunManager); + const params = actionsClient.execute.mock.calls[0][0].params.subActionParams as unknown as { + messages: GeminiContent[]; + systemInstruction: string; + }; + expect(params.messages.length).toEqual(1); + expect(params.messages[0].parts.length).toEqual(1); + expect(params.systemInstruction).toEqual(systemInstruction); + }); + it('Handles 2 messages in a row from the same role', async () => { + const actionsClientChatVertexAI = new ActionsClientChatVertexAI(defaultArgs); + + await actionsClientChatVertexAI._generate( + [...callMessages, new HumanMessage('Oh boy, another')], + callOptions, + callRunManager + ); + const { messages } = actionsClient.execute.mock.calls[0][0].params + .subActionParams as unknown as { messages: GeminiContent[] }; + expect(messages.length).toEqual(1); + expect(messages[0].parts.length).toEqual(2); + }); + }); }); diff --git a/x-pack/packages/kbn-langchain/server/language_models/chat_vertex/connection.ts b/x-pack/packages/kbn-langchain/server/language_models/chat_vertex/connection.ts index 0340d71b438db..dd3c1e1abdda0 100644 --- a/x-pack/packages/kbn-langchain/server/language_models/chat_vertex/connection.ts +++ b/x-pack/packages/kbn-langchain/server/language_models/chat_vertex/connection.ts @@ -7,6 +7,7 @@ import { ChatConnection, + GeminiContent, GoogleAbstractedClient, GoogleAIBaseLLMInput, GoogleLLMResponse, @@ -39,6 +40,22 @@ export class ActionsClientChatConnection extends ChatConnection { this.caller = caller; this.#model = fields.model; this.temperature = fields.temperature ?? 0; + const nativeFormatData = this.formatData.bind(this); + this.formatData = async (data, options) => { + const result = await nativeFormatData(data, options); + if (result?.contents != null && result?.contents.length) { + // ensure there are not 2 messages in a row from the same role, + // if there are combine them + result.contents = result.contents.reduce((acc: GeminiContent[], currentEntry) => { + if (currentEntry.role === acc[acc.length - 1]?.role) { + acc[acc.length - 1].parts = acc[acc.length - 1].parts.concat(currentEntry.parts); + return acc; + } + return [...acc, currentEntry]; + }, []); + } + return result; + }; } async _request( diff --git a/x-pack/packages/ml/trained_models_utils/src/constants/trained_models.ts b/x-pack/packages/ml/trained_models_utils/src/constants/trained_models.ts index 95337518361e9..9fd3483771a9f 100644 --- a/x-pack/packages/ml/trained_models_utils/src/constants/trained_models.ts +++ b/x-pack/packages/ml/trained_models_utils/src/constants/trained_models.ts @@ -120,6 +120,10 @@ export const ELASTIC_MODEL_DEFINITIONS: Record< license: 'MIT', licenseUrl: 'https://huggingface.co/elastic/multilingual-e5-small', type: ['pytorch', 'text_embedding'], + disclaimer: i18n.translate('xpack.ml.trainedModels.modelsList.e5v1Disclaimer', { + defaultMessage: + 'This E5 model, as defined, hosted, integrated and used in conjunction with our other Elastic Software is covered by our standard warranty.', + }), }, [E5_LINUX_OPTIMIZED_MODEL_ID]: { modelName: 'e5', @@ -138,6 +142,10 @@ export const ELASTIC_MODEL_DEFINITIONS: Record< license: 'MIT', licenseUrl: 'https://huggingface.co/elastic/multilingual-e5-small_linux-x86_64', type: ['pytorch', 'text_embedding'], + disclaimer: i18n.translate('xpack.ml.trainedModels.modelsList.e5v1Disclaimer', { + defaultMessage: + 'This E5 model, as defined, hosted, integrated and used in conjunction with our other Elastic Software is covered by our standard warranty.', + }), }, } as const); @@ -167,6 +175,7 @@ export interface ModelDefinition { /** Link to the external license/documentation page */ licenseUrl?: string; type?: readonly string[]; + disclaimer?: string; } export type ModelDefinitionResponse = ModelDefinition & { diff --git a/x-pack/packages/observability/logs_overview/README.md b/x-pack/packages/observability/logs_overview/README.md new file mode 100644 index 0000000000000..20d3f0f02b7df --- /dev/null +++ b/x-pack/packages/observability/logs_overview/README.md @@ -0,0 +1,3 @@ +# @kbn/observability-logs-overview + +Empty package generated by @kbn/generate diff --git a/x-pack/packages/observability/logs_overview/index.ts b/x-pack/packages/observability/logs_overview/index.ts new file mode 100644 index 0000000000000..057d1d3acd152 --- /dev/null +++ b/x-pack/packages/observability/logs_overview/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; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +export { + LogsOverview, + LogsOverviewErrorContent, + LogsOverviewLoadingContent, + type LogsOverviewDependencies, + type LogsOverviewErrorContentProps, + type LogsOverviewProps, +} from './src/components/logs_overview'; +export type { + DataViewLogsSourceConfiguration, + IndexNameLogsSourceConfiguration, + LogsSourceConfiguration, + SharedSettingLogsSourceConfiguration, +} from './src/utils/logs_source'; diff --git a/x-pack/packages/observability/logs_overview/jest.config.js b/x-pack/packages/observability/logs_overview/jest.config.js new file mode 100644 index 0000000000000..2ee88ee990253 --- /dev/null +++ b/x-pack/packages/observability/logs_overview/jest.config.js @@ -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. + */ + +module.exports = { + preset: '@kbn/test', + rootDir: '../../../..', + roots: ['/x-pack/packages/observability/logs_overview'], +}; diff --git a/x-pack/packages/observability/logs_overview/kibana.jsonc b/x-pack/packages/observability/logs_overview/kibana.jsonc new file mode 100644 index 0000000000000..90b3375086720 --- /dev/null +++ b/x-pack/packages/observability/logs_overview/kibana.jsonc @@ -0,0 +1,5 @@ +{ + "type": "shared-browser", + "id": "@kbn/observability-logs-overview", + "owner": "@elastic/obs-ux-logs-team" +} diff --git a/x-pack/packages/observability/logs_overview/package.json b/x-pack/packages/observability/logs_overview/package.json new file mode 100644 index 0000000000000..77a529e7e59f7 --- /dev/null +++ b/x-pack/packages/observability/logs_overview/package.json @@ -0,0 +1,7 @@ +{ + "name": "@kbn/observability-logs-overview", + "private": true, + "version": "1.0.0", + "license": "Elastic License 2.0", + "sideEffects": false +} diff --git a/x-pack/packages/observability/logs_overview/src/components/discover_link/discover_link.tsx b/x-pack/packages/observability/logs_overview/src/components/discover_link/discover_link.tsx new file mode 100644 index 0000000000000..fe108289985a9 --- /dev/null +++ b/x-pack/packages/observability/logs_overview/src/components/discover_link/discover_link.tsx @@ -0,0 +1,110 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import type { QueryDslQueryContainer } from '@elastic/elasticsearch/lib/api/types'; +import { EuiButton } from '@elastic/eui'; +import type { DiscoverAppLocatorParams } from '@kbn/discover-plugin/common'; +import { FilterStateStore, buildCustomFilter } from '@kbn/es-query'; +import { i18n } from '@kbn/i18n'; +import { getRouterLinkProps } from '@kbn/router-utils'; +import type { SharePluginStart } from '@kbn/share-plugin/public'; +import React, { useCallback, useMemo } from 'react'; +import type { IndexNameLogsSourceConfiguration } from '../../utils/logs_source'; + +export interface DiscoverLinkProps { + documentFilters?: QueryDslQueryContainer[]; + logsSource: IndexNameLogsSourceConfiguration; + timeRange: { + start: string; + end: string; + }; + dependencies: DiscoverLinkDependencies; +} + +export interface DiscoverLinkDependencies { + share: SharePluginStart; +} + +export const DiscoverLink = React.memo( + ({ dependencies: { share }, documentFilters, logsSource, timeRange }: DiscoverLinkProps) => { + const discoverLocatorParams = useMemo( + () => ({ + dataViewSpec: { + id: logsSource.indexName, + name: logsSource.indexName, + title: logsSource.indexName, + timeFieldName: logsSource.timestampField, + }, + timeRange: { + from: timeRange.start, + to: timeRange.end, + }, + filters: documentFilters?.map((filter) => + buildCustomFilter( + logsSource.indexName, + filter, + false, + false, + categorizedLogsFilterLabel, + FilterStateStore.APP_STATE + ) + ), + }), + [ + documentFilters, + logsSource.indexName, + logsSource.timestampField, + timeRange.end, + timeRange.start, + ] + ); + + const discoverLocator = useMemo( + () => share.url.locators.get('DISCOVER_APP_LOCATOR'), + [share.url.locators] + ); + + const discoverUrl = useMemo( + () => discoverLocator?.getRedirectUrl(discoverLocatorParams), + [discoverLocatorParams, discoverLocator] + ); + + const navigateToDiscover = useCallback(() => { + discoverLocator?.navigate(discoverLocatorParams); + }, [discoverLocatorParams, discoverLocator]); + + const discoverLinkProps = getRouterLinkProps({ + href: discoverUrl, + onClick: navigateToDiscover, + }); + + return ( + + {discoverLinkTitle} + + ); + } +); + +export const discoverLinkTitle = i18n.translate( + 'xpack.observabilityLogsOverview.discoverLinkTitle', + { + defaultMessage: 'Open in Discover', + } +); + +export const categorizedLogsFilterLabel = i18n.translate( + 'xpack.observabilityLogsOverview.categorizedLogsFilterLabel', + { + defaultMessage: 'Categorized log entries', + } +); diff --git a/x-pack/plugins/security_solution_serverless/server/common/services/index.ts b/x-pack/packages/observability/logs_overview/src/components/discover_link/index.ts similarity index 79% rename from x-pack/plugins/security_solution_serverless/server/common/services/index.ts rename to x-pack/packages/observability/logs_overview/src/components/discover_link/index.ts index a76f6359f7e5b..738bf51d4529d 100644 --- a/x-pack/plugins/security_solution_serverless/server/common/services/index.ts +++ b/x-pack/packages/observability/logs_overview/src/components/discover_link/index.ts @@ -5,4 +5,4 @@ * 2.0. */ -export { usageReportingService } from './usage_reporting_service'; +export * from './discover_link'; diff --git a/x-pack/packages/observability/logs_overview/src/components/log_categories/index.ts b/x-pack/packages/observability/logs_overview/src/components/log_categories/index.ts new file mode 100644 index 0000000000000..786475396237c --- /dev/null +++ b/x-pack/packages/observability/logs_overview/src/components/log_categories/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 './log_categories'; diff --git a/x-pack/packages/observability/logs_overview/src/components/log_categories/log_categories.tsx b/x-pack/packages/observability/logs_overview/src/components/log_categories/log_categories.tsx new file mode 100644 index 0000000000000..6204667827281 --- /dev/null +++ b/x-pack/packages/observability/logs_overview/src/components/log_categories/log_categories.tsx @@ -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 { QueryDslQueryContainer } from '@elastic/elasticsearch/lib/api/types'; +import { ISearchGeneric } from '@kbn/search-types'; +import { createConsoleInspector } from '@kbn/xstate-utils'; +import { useMachine } from '@xstate5/react'; +import React, { useCallback } from 'react'; +import { + categorizeLogsService, + createCategorizeLogsServiceImplementations, +} from '../../services/categorize_logs_service'; +import { IndexNameLogsSourceConfiguration } from '../../utils/logs_source'; +import { LogCategoriesErrorContent } from './log_categories_error_content'; +import { LogCategoriesLoadingContent } from './log_categories_loading_content'; +import { + LogCategoriesResultContent, + LogCategoriesResultContentDependencies, +} from './log_categories_result_content'; + +export interface LogCategoriesProps { + dependencies: LogCategoriesDependencies; + documentFilters?: QueryDslQueryContainer[]; + logsSource: IndexNameLogsSourceConfiguration; + // The time range could be made optional if we want to support an internal + // time range picker + timeRange: { + start: string; + end: string; + }; +} + +export type LogCategoriesDependencies = LogCategoriesResultContentDependencies & { + search: ISearchGeneric; +}; + +export const LogCategories: React.FC = ({ + dependencies, + documentFilters = [], + logsSource, + timeRange, +}) => { + const [categorizeLogsServiceState, sendToCategorizeLogsService] = useMachine( + categorizeLogsService.provide( + createCategorizeLogsServiceImplementations({ search: dependencies.search }) + ), + { + inspect: consoleInspector, + input: { + index: logsSource.indexName, + startTimestamp: timeRange.start, + endTimestamp: timeRange.end, + timeField: logsSource.timestampField, + messageField: logsSource.messageField, + documentFilters, + }, + } + ); + + const cancelOperation = useCallback(() => { + sendToCategorizeLogsService({ + type: 'cancel', + }); + }, [sendToCategorizeLogsService]); + + if (categorizeLogsServiceState.matches('done')) { + return ( + + ); + } else if (categorizeLogsServiceState.matches('failed')) { + return ; + } else if (categorizeLogsServiceState.matches('countingDocuments')) { + return ; + } else if ( + categorizeLogsServiceState.matches('fetchingSampledCategories') || + categorizeLogsServiceState.matches('fetchingRemainingCategories') + ) { + return ; + } else { + return null; + } +}; + +const consoleInspector = createConsoleInspector(); diff --git a/x-pack/packages/observability/logs_overview/src/components/log_categories/log_categories_control_bar.tsx b/x-pack/packages/observability/logs_overview/src/components/log_categories/log_categories_control_bar.tsx new file mode 100644 index 0000000000000..4538b0ec2fd5d --- /dev/null +++ b/x-pack/packages/observability/logs_overview/src/components/log_categories/log_categories_control_bar.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 type { QueryDslQueryContainer } from '@elastic/elasticsearch/lib/api/types'; +import { EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; +import type { SharePluginStart } from '@kbn/share-plugin/public'; +import React from 'react'; +import type { IndexNameLogsSourceConfiguration } from '../../utils/logs_source'; +import { DiscoverLink } from '../discover_link'; + +export interface LogCategoriesControlBarProps { + documentFilters?: QueryDslQueryContainer[]; + logsSource: IndexNameLogsSourceConfiguration; + timeRange: { + start: string; + end: string; + }; + dependencies: LogCategoriesControlBarDependencies; +} + +export interface LogCategoriesControlBarDependencies { + share: SharePluginStart; +} + +export const LogCategoriesControlBar: React.FC = React.memo( + ({ dependencies, documentFilters, logsSource, timeRange }) => { + return ( + + + + + + ); + } +); diff --git a/x-pack/packages/observability/logs_overview/src/components/log_categories/log_categories_error_content.tsx b/x-pack/packages/observability/logs_overview/src/components/log_categories/log_categories_error_content.tsx new file mode 100644 index 0000000000000..1a335e3265294 --- /dev/null +++ b/x-pack/packages/observability/logs_overview/src/components/log_categories/log_categories_error_content.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 { EuiCodeBlock, EuiEmptyPrompt } from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; +import React from 'react'; + +export interface LogCategoriesErrorContentProps { + error?: Error; +} + +export const LogCategoriesErrorContent: React.FC = ({ error }) => { + return ( + {logsOverviewErrorTitle}} + body={ + +

    {error?.stack ?? error?.toString() ?? unknownErrorDescription}

    +
    + } + layout="vertical" + /> + ); +}; + +const logsOverviewErrorTitle = i18n.translate( + 'xpack.observabilityLogsOverview.logCategories.errorTitle', + { + defaultMessage: 'Failed to categorize logs', + } +); + +const unknownErrorDescription = i18n.translate( + 'xpack.observabilityLogsOverview.logCategories.unknownErrorDescription', + { + defaultMessage: 'An unspecified error occurred.', + } +); diff --git a/x-pack/packages/observability/logs_overview/src/components/log_categories/log_categories_grid.tsx b/x-pack/packages/observability/logs_overview/src/components/log_categories/log_categories_grid.tsx new file mode 100644 index 0000000000000..d9e960685de99 --- /dev/null +++ b/x-pack/packages/observability/logs_overview/src/components/log_categories/log_categories_grid.tsx @@ -0,0 +1,182 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { + EuiDataGrid, + EuiDataGridColumnSortingConfig, + EuiDataGridPaginationProps, +} from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; +import { createConsoleInspector } from '@kbn/xstate-utils'; +import { useMachine } from '@xstate5/react'; +import _ from 'lodash'; +import React, { useMemo } from 'react'; +import { assign, setup } from 'xstate5'; +import { LogCategory } from '../../types'; +import { + LogCategoriesGridCellDependencies, + LogCategoriesGridColumnId, + createCellContext, + logCategoriesGridColumnIds, + logCategoriesGridColumns, + renderLogCategoriesGridCell, +} from './log_categories_grid_cell'; + +export interface LogCategoriesGridProps { + dependencies: LogCategoriesGridDependencies; + logCategories: LogCategory[]; +} + +export type LogCategoriesGridDependencies = LogCategoriesGridCellDependencies; + +export const LogCategoriesGrid: React.FC = ({ + dependencies, + logCategories, +}) => { + const [gridState, dispatchGridEvent] = useMachine(gridStateService, { + input: { + visibleColumns: logCategoriesGridColumns.map(({ id }) => id), + }, + inspect: consoleInspector, + }); + + const sortedLogCategories = useMemo(() => { + const sortingCriteria = gridState.context.sortingColumns.map( + ({ id, direction }): [(logCategory: LogCategory) => any, 'asc' | 'desc'] => { + switch (id) { + case 'count': + return [(logCategory: LogCategory) => logCategory.documentCount, direction]; + case 'change_type': + // TODO: use better sorting weight for change types + return [(logCategory: LogCategory) => logCategory.change.type, direction]; + case 'change_time': + return [ + (logCategory: LogCategory) => + 'timestamp' in logCategory.change ? logCategory.change.timestamp ?? '' : '', + direction, + ]; + default: + return [_.identity, direction]; + } + } + ); + return _.orderBy( + logCategories, + sortingCriteria.map(([accessor]) => accessor), + sortingCriteria.map(([, direction]) => direction) + ); + }, [gridState.context.sortingColumns, logCategories]); + + return ( + + dispatchGridEvent({ type: 'changeVisibleColumns', visibleColumns }), + }} + cellContext={createCellContext(sortedLogCategories, dependencies)} + pagination={{ + ...gridState.context.pagination, + onChangeItemsPerPage: (pageSize) => dispatchGridEvent({ type: 'changePageSize', pageSize }), + onChangePage: (pageIndex) => dispatchGridEvent({ type: 'changePageIndex', pageIndex }), + }} + renderCellValue={renderLogCategoriesGridCell} + rowCount={sortedLogCategories.length} + sorting={{ + columns: gridState.context.sortingColumns, + onSort: (sortingColumns) => + dispatchGridEvent({ type: 'changeSortingColumns', sortingColumns }), + }} + /> + ); +}; + +const gridStateService = setup({ + types: { + context: {} as { + visibleColumns: string[]; + pagination: Pick; + sortingColumns: LogCategoriesGridSortingConfig[]; + }, + events: {} as + | { + type: 'changePageSize'; + pageSize: number; + } + | { + type: 'changePageIndex'; + pageIndex: number; + } + | { + type: 'changeSortingColumns'; + sortingColumns: EuiDataGridColumnSortingConfig[]; + } + | { + type: 'changeVisibleColumns'; + visibleColumns: string[]; + }, + input: {} as { + visibleColumns: string[]; + }, + }, +}).createMachine({ + id: 'logCategoriesGridState', + context: ({ input }) => ({ + visibleColumns: input.visibleColumns, + pagination: { pageIndex: 0, pageSize: 20, pageSizeOptions: [10, 20, 50] }, + sortingColumns: [{ id: 'change_time', direction: 'desc' }], + }), + on: { + changePageSize: { + actions: assign(({ context, event }) => ({ + pagination: { + ...context.pagination, + pageIndex: 0, + pageSize: event.pageSize, + }, + })), + }, + changePageIndex: { + actions: assign(({ context, event }) => ({ + pagination: { + ...context.pagination, + pageIndex: event.pageIndex, + }, + })), + }, + changeSortingColumns: { + actions: assign(({ event }) => ({ + sortingColumns: event.sortingColumns.filter( + (sortingConfig): sortingConfig is LogCategoriesGridSortingConfig => + (logCategoriesGridColumnIds as string[]).includes(sortingConfig.id) + ), + })), + }, + changeVisibleColumns: { + actions: assign(({ event }) => ({ + visibleColumns: event.visibleColumns, + })), + }, + }, +}); + +const consoleInspector = createConsoleInspector(); + +const logCategoriesGridLabel = i18n.translate( + 'xpack.observabilityLogsOverview.logCategoriesGrid.euiDataGrid.logCategoriesLabel', + { defaultMessage: 'Log categories' } +); + +interface TypedEuiDataGridColumnSortingConfig + extends EuiDataGridColumnSortingConfig { + id: ColumnId; +} + +type LogCategoriesGridSortingConfig = + TypedEuiDataGridColumnSortingConfig; diff --git a/x-pack/packages/observability/logs_overview/src/components/log_categories/log_categories_grid_cell.tsx b/x-pack/packages/observability/logs_overview/src/components/log_categories/log_categories_grid_cell.tsx new file mode 100644 index 0000000000000..d6ab4969eaf7b --- /dev/null +++ b/x-pack/packages/observability/logs_overview/src/components/log_categories/log_categories_grid_cell.tsx @@ -0,0 +1,99 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { EuiDataGridColumn, RenderCellValue } from '@elastic/eui'; +import React from 'react'; +import { LogCategory } from '../../types'; +import { + LogCategoriesGridChangeTimeCell, + LogCategoriesGridChangeTimeCellDependencies, + logCategoriesGridChangeTimeColumn, +} from './log_categories_grid_change_time_cell'; +import { + LogCategoriesGridChangeTypeCell, + logCategoriesGridChangeTypeColumn, +} from './log_categories_grid_change_type_cell'; +import { + LogCategoriesGridCountCell, + logCategoriesGridCountColumn, +} from './log_categories_grid_count_cell'; +import { + LogCategoriesGridHistogramCell, + LogCategoriesGridHistogramCellDependencies, + logCategoriesGridHistoryColumn, +} from './log_categories_grid_histogram_cell'; +import { + LogCategoriesGridPatternCell, + logCategoriesGridPatternColumn, +} from './log_categories_grid_pattern_cell'; + +export interface LogCategoriesGridCellContext { + dependencies: LogCategoriesGridCellDependencies; + logCategories: LogCategory[]; +} + +export type LogCategoriesGridCellDependencies = LogCategoriesGridHistogramCellDependencies & + LogCategoriesGridChangeTimeCellDependencies; + +export const renderLogCategoriesGridCell: RenderCellValue = ({ + rowIndex, + columnId, + isExpanded, + ...rest +}) => { + const { dependencies, logCategories } = getCellContext(rest); + + const logCategory = logCategories[rowIndex]; + + switch (columnId as LogCategoriesGridColumnId) { + case 'pattern': + return ; + case 'count': + return ; + case 'history': + return ( + + ); + case 'change_type': + return ; + case 'change_time': + return ( + + ); + default: + return <>-; + } +}; + +export const logCategoriesGridColumns = [ + logCategoriesGridPatternColumn, + logCategoriesGridCountColumn, + logCategoriesGridChangeTypeColumn, + logCategoriesGridChangeTimeColumn, + logCategoriesGridHistoryColumn, +] satisfies EuiDataGridColumn[]; + +export const logCategoriesGridColumnIds = logCategoriesGridColumns.map(({ id }) => id); + +export type LogCategoriesGridColumnId = (typeof logCategoriesGridColumns)[number]['id']; + +const cellContextKey = 'cellContext'; + +const getCellContext = (cellContext: object): LogCategoriesGridCellContext => + (cellContextKey in cellContext + ? cellContext[cellContextKey] + : {}) as LogCategoriesGridCellContext; + +export const createCellContext = ( + logCategories: LogCategory[], + dependencies: LogCategoriesGridCellDependencies +): { [cellContextKey]: LogCategoriesGridCellContext } => ({ + [cellContextKey]: { + dependencies, + logCategories, + }, +}); diff --git a/x-pack/packages/observability/logs_overview/src/components/log_categories/log_categories_grid_change_time_cell.tsx b/x-pack/packages/observability/logs_overview/src/components/log_categories/log_categories_grid_change_time_cell.tsx new file mode 100644 index 0000000000000..5ad8cbdd49346 --- /dev/null +++ b/x-pack/packages/observability/logs_overview/src/components/log_categories/log_categories_grid_change_time_cell.tsx @@ -0,0 +1,54 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { EuiDataGridColumn } from '@elastic/eui'; +import { SettingsStart } from '@kbn/core-ui-settings-browser'; +import { i18n } from '@kbn/i18n'; +import moment from 'moment'; +import React, { useMemo } from 'react'; +import { LogCategory } from '../../types'; + +export const logCategoriesGridChangeTimeColumn = { + id: 'change_time' as const, + display: i18n.translate( + 'xpack.observabilityLogsOverview.logCategoriesGrid.changeTimeColumnLabel', + { + defaultMessage: 'Change at', + } + ), + isSortable: true, + initialWidth: 220, + schema: 'datetime', +} satisfies EuiDataGridColumn; + +export interface LogCategoriesGridChangeTimeCellProps { + dependencies: LogCategoriesGridChangeTimeCellDependencies; + logCategory: LogCategory; +} + +export interface LogCategoriesGridChangeTimeCellDependencies { + uiSettings: SettingsStart; +} + +export const LogCategoriesGridChangeTimeCell: React.FC = ({ + dependencies, + logCategory, +}) => { + const dateFormat = useMemo( + () => dependencies.uiSettings.client.get('dateFormat'), + [dependencies.uiSettings.client] + ); + if (!('timestamp' in logCategory.change && logCategory.change.timestamp != null)) { + return null; + } + + if (dateFormat) { + return <>{moment(logCategory.change.timestamp).format(dateFormat)}; + } else { + return <>{logCategory.change.timestamp}; + } +}; diff --git a/x-pack/packages/observability/logs_overview/src/components/log_categories/log_categories_grid_change_type_cell.tsx b/x-pack/packages/observability/logs_overview/src/components/log_categories/log_categories_grid_change_type_cell.tsx new file mode 100644 index 0000000000000..af6349bd0e18c --- /dev/null +++ b/x-pack/packages/observability/logs_overview/src/components/log_categories/log_categories_grid_change_type_cell.tsx @@ -0,0 +1,108 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { EuiBadge, EuiDataGridColumn } from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; +import React from 'react'; +import { LogCategory } from '../../types'; + +export const logCategoriesGridChangeTypeColumn = { + id: 'change_type' as const, + display: i18n.translate( + 'xpack.observabilityLogsOverview.logCategoriesGrid.changeTypeColumnLabel', + { + defaultMessage: 'Change type', + } + ), + isSortable: true, + initialWidth: 110, +} satisfies EuiDataGridColumn; + +export interface LogCategoriesGridChangeTypeCellProps { + logCategory: LogCategory; +} + +export const LogCategoriesGridChangeTypeCell: React.FC = ({ + logCategory, +}) => { + switch (logCategory.change.type) { + case 'dip': + return {dipBadgeLabel}; + case 'spike': + return {spikeBadgeLabel}; + case 'step': + return {stepBadgeLabel}; + case 'distribution': + return {distributionBadgeLabel}; + case 'rare': + return {rareBadgeLabel}; + case 'trend': + return {trendBadgeLabel}; + case 'other': + return {otherBadgeLabel}; + case 'none': + return <>-; + default: + return {unknownBadgeLabel}; + } +}; + +const dipBadgeLabel = i18n.translate( + 'xpack.observabilityLogsOverview.logCategories.dipChangeTypeBadgeLabel', + { + defaultMessage: 'Dip', + } +); + +const spikeBadgeLabel = i18n.translate( + 'xpack.observabilityLogsOverview.logCategories.spikeChangeTypeBadgeLabel', + { + defaultMessage: 'Spike', + } +); + +const stepBadgeLabel = i18n.translate( + 'xpack.observabilityLogsOverview.logCategories.spikeChangeTypeBadgeLabel', + { + defaultMessage: 'Step', + } +); + +const distributionBadgeLabel = i18n.translate( + 'xpack.observabilityLogsOverview.logCategories.distributionChangeTypeBadgeLabel', + { + defaultMessage: 'Distribution', + } +); + +const trendBadgeLabel = i18n.translate( + 'xpack.observabilityLogsOverview.logCategories.spikeChangeTypeBadgeLabel', + { + defaultMessage: 'Trend', + } +); + +const otherBadgeLabel = i18n.translate( + 'xpack.observabilityLogsOverview.logCategories.otherChangeTypeBadgeLabel', + { + defaultMessage: 'Other', + } +); + +const unknownBadgeLabel = i18n.translate( + 'xpack.observabilityLogsOverview.logCategories.unknownChangeTypeBadgeLabel', + { + defaultMessage: 'Unknown', + } +); + +const rareBadgeLabel = i18n.translate( + 'xpack.observabilityLogsOverview.logCategories.rareChangeTypeBadgeLabel', + { + defaultMessage: 'Rare', + } +); diff --git a/x-pack/packages/observability/logs_overview/src/components/log_categories/log_categories_grid_count_cell.tsx b/x-pack/packages/observability/logs_overview/src/components/log_categories/log_categories_grid_count_cell.tsx new file mode 100644 index 0000000000000..f2247aab5212e --- /dev/null +++ b/x-pack/packages/observability/logs_overview/src/components/log_categories/log_categories_grid_count_cell.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 { EuiDataGridColumn } from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; +import { FormattedNumber } from '@kbn/i18n-react'; +import React from 'react'; +import { LogCategory } from '../../types'; + +export const logCategoriesGridCountColumn = { + id: 'count' as const, + display: i18n.translate('xpack.observabilityLogsOverview.logCategoriesGrid.countColumnLabel', { + defaultMessage: 'Events', + }), + isSortable: true, + schema: 'numeric', + initialWidth: 100, +} satisfies EuiDataGridColumn; + +export interface LogCategoriesGridCountCellProps { + logCategory: LogCategory; +} + +export const LogCategoriesGridCountCell: React.FC = ({ + logCategory, +}) => { + return ; +}; diff --git a/x-pack/packages/observability/logs_overview/src/components/log_categories/log_categories_grid_histogram_cell.tsx b/x-pack/packages/observability/logs_overview/src/components/log_categories/log_categories_grid_histogram_cell.tsx new file mode 100644 index 0000000000000..2fb50b0f2f3b4 --- /dev/null +++ b/x-pack/packages/observability/logs_overview/src/components/log_categories/log_categories_grid_histogram_cell.tsx @@ -0,0 +1,99 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { + BarSeries, + Chart, + LineAnnotation, + LineAnnotationStyle, + PartialTheme, + Settings, + Tooltip, + TooltipType, +} from '@elastic/charts'; +import { EuiDataGridColumn } from '@elastic/eui'; +import { ChartsPluginStart } from '@kbn/charts-plugin/public'; +import { i18n } from '@kbn/i18n'; +import { RecursivePartial } from '@kbn/utility-types'; +import React from 'react'; +import { LogCategory, LogCategoryHistogramBucket } from '../../types'; + +export const logCategoriesGridHistoryColumn = { + id: 'history' as const, + display: i18n.translate( + 'xpack.observabilityLogsOverview.logCategoriesGrid.histogramColumnLabel', + { + defaultMessage: 'Timeline', + } + ), + isSortable: false, + initialWidth: 250, + isExpandable: false, +} satisfies EuiDataGridColumn; + +export interface LogCategoriesGridHistogramCellProps { + dependencies: LogCategoriesGridHistogramCellDependencies; + logCategory: LogCategory; +} + +export interface LogCategoriesGridHistogramCellDependencies { + charts: ChartsPluginStart; +} + +export const LogCategoriesGridHistogramCell: React.FC = ({ + dependencies: { charts }, + logCategory, +}) => { + const baseTheme = charts.theme.useChartsBaseTheme(); + const sparklineTheme = charts.theme.useSparklineOverrides(); + + return ( + + + + + {'timestamp' in logCategory.change && logCategory.change.timestamp != null ? ( + + ) : null} + + ); +}; + +const localThemeOverrides: PartialTheme = { + scales: { + histogramPadding: 0.1, + }, + background: { + color: 'transparent', + }, +}; + +const annotationStyle: RecursivePartial = { + line: { + strokeWidth: 2, + }, +}; + +const timestampAccessor = (histogram: LogCategoryHistogramBucket) => + new Date(histogram.timestamp).getTime(); diff --git a/x-pack/packages/observability/logs_overview/src/components/log_categories/log_categories_grid_pattern_cell.tsx b/x-pack/packages/observability/logs_overview/src/components/log_categories/log_categories_grid_pattern_cell.tsx new file mode 100644 index 0000000000000..d507487a99e3c --- /dev/null +++ b/x-pack/packages/observability/logs_overview/src/components/log_categories/log_categories_grid_pattern_cell.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 { EuiDataGridColumn, useEuiTheme } from '@elastic/eui'; +import { css } from '@emotion/react'; +import { i18n } from '@kbn/i18n'; +import React, { useMemo } from 'react'; +import { LogCategory } from '../../types'; + +export const logCategoriesGridPatternColumn = { + id: 'pattern' as const, + display: i18n.translate('xpack.observabilityLogsOverview.logCategoriesGrid.patternColumnLabel', { + defaultMessage: 'Pattern', + }), + isSortable: false, + schema: 'string', +} satisfies EuiDataGridColumn; + +export interface LogCategoriesGridPatternCellProps { + logCategory: LogCategory; +} + +export const LogCategoriesGridPatternCell: React.FC = ({ + logCategory, +}) => { + const theme = useEuiTheme(); + const { euiTheme } = theme; + const termsList = useMemo(() => logCategory.terms.split(' '), [logCategory.terms]); + + const commonStyle = css` + display: inline-block; + font-family: ${euiTheme.font.familyCode}; + margin-right: ${euiTheme.size.xs}; + `; + + const termStyle = css` + ${commonStyle}; + `; + + const separatorStyle = css` + ${commonStyle}; + color: ${euiTheme.colors.successText}; + `; + + return ( +
    +      
    *
    + {termsList.map((term, index) => ( + +
    {term}
    +
    *
    +
    + ))} +
    + ); +}; diff --git a/x-pack/packages/observability/logs_overview/src/components/log_categories/log_categories_loading_content.tsx b/x-pack/packages/observability/logs_overview/src/components/log_categories/log_categories_loading_content.tsx new file mode 100644 index 0000000000000..0fde469fe717d --- /dev/null +++ b/x-pack/packages/observability/logs_overview/src/components/log_categories/log_categories_loading_content.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 { EuiButton, EuiEmptyPrompt, EuiLoadingSpinner } from '@elastic/eui'; +import React from 'react'; +import { i18n } from '@kbn/i18n'; + +export interface LogCategoriesLoadingContentProps { + onCancel?: () => void; + stage: 'counting' | 'categorizing'; +} + +export const LogCategoriesLoadingContent: React.FC = ({ + onCancel, + stage, +}) => { + return ( + } + title={ +

    + {stage === 'counting' + ? logCategoriesLoadingStateCountingTitle + : logCategoriesLoadingStateCategorizingTitle} +

    + } + actions={ + onCancel != null + ? [ + { + onCancel(); + }} + > + {logCategoriesLoadingStateCancelButtonLabel} + , + ] + : [] + } + /> + ); +}; + +const logCategoriesLoadingStateCountingTitle = i18n.translate( + 'xpack.observabilityLogsOverview.logCategoriesGrid.loadingStageCountingTitle', + { + defaultMessage: 'Estimating log volume', + } +); + +const logCategoriesLoadingStateCategorizingTitle = i18n.translate( + 'xpack.observabilityLogsOverview.logCategoriesGrid.loadingStageCategorizingTitle', + { + defaultMessage: 'Categorizing logs', + } +); + +const logCategoriesLoadingStateCancelButtonLabel = i18n.translate( + 'xpack.observabilityLogsOverview.logCategoriesGrid.loadingStateCancelButtonLabel', + { + defaultMessage: 'Cancel', + } +); diff --git a/x-pack/packages/observability/logs_overview/src/components/log_categories/log_categories_result_content.tsx b/x-pack/packages/observability/logs_overview/src/components/log_categories/log_categories_result_content.tsx new file mode 100644 index 0000000000000..e16bdda7cb44a --- /dev/null +++ b/x-pack/packages/observability/logs_overview/src/components/log_categories/log_categories_result_content.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 type { QueryDslQueryContainer } from '@elastic/elasticsearch/lib/api/types'; +import { EuiEmptyPrompt, EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; +import React from 'react'; +import { LogCategory } from '../../types'; +import { IndexNameLogsSourceConfiguration } from '../../utils/logs_source'; +import { + LogCategoriesControlBar, + LogCategoriesControlBarDependencies, +} from './log_categories_control_bar'; +import { LogCategoriesGrid, LogCategoriesGridDependencies } from './log_categories_grid'; + +export interface LogCategoriesResultContentProps { + dependencies: LogCategoriesResultContentDependencies; + documentFilters?: QueryDslQueryContainer[]; + logCategories: LogCategory[]; + logsSource: IndexNameLogsSourceConfiguration; + timeRange: { + start: string; + end: string; + }; +} + +export type LogCategoriesResultContentDependencies = LogCategoriesControlBarDependencies & + LogCategoriesGridDependencies; + +export const LogCategoriesResultContent: React.FC = ({ + dependencies, + documentFilters, + logCategories, + logsSource, + timeRange, +}) => { + if (logCategories.length === 0) { + return ; + } else { + return ( + + + + + + + + + ); + } +}; + +export const LogCategoriesEmptyResultContent: React.FC = () => { + return ( + {emptyResultContentDescription}

    } + color="subdued" + layout="horizontal" + title={

    {emptyResultContentTitle}

    } + titleSize="m" + /> + ); +}; + +const emptyResultContentTitle = i18n.translate( + 'xpack.observabilityLogsOverview.logCategories.emptyResultContentTitle', + { + defaultMessage: 'No log categories found', + } +); + +const emptyResultContentDescription = i18n.translate( + 'xpack.observabilityLogsOverview.logCategories.emptyResultContentDescription', + { + defaultMessage: + 'No suitable documents within the time range. Try searching for a longer time period.', + } +); diff --git a/x-pack/packages/observability/logs_overview/src/components/logs_overview/index.ts b/x-pack/packages/observability/logs_overview/src/components/logs_overview/index.ts new file mode 100644 index 0000000000000..878f634f078ad --- /dev/null +++ b/x-pack/packages/observability/logs_overview/src/components/logs_overview/index.ts @@ -0,0 +1,10 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +export * from './logs_overview'; +export * from './logs_overview_error_content'; +export * from './logs_overview_loading_content'; diff --git a/x-pack/packages/observability/logs_overview/src/components/logs_overview/logs_overview.tsx b/x-pack/packages/observability/logs_overview/src/components/logs_overview/logs_overview.tsx new file mode 100644 index 0000000000000..988656eb1571e --- /dev/null +++ b/x-pack/packages/observability/logs_overview/src/components/logs_overview/logs_overview.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 { QueryDslQueryContainer } from '@elastic/elasticsearch/lib/api/types'; +import { type LogsDataAccessPluginStart } from '@kbn/logs-data-access-plugin/public'; +import React from 'react'; +import useAsync from 'react-use/lib/useAsync'; +import { LogsSourceConfiguration, normalizeLogsSource } from '../../utils/logs_source'; +import { LogCategories, LogCategoriesDependencies } from '../log_categories'; +import { LogsOverviewErrorContent } from './logs_overview_error_content'; +import { LogsOverviewLoadingContent } from './logs_overview_loading_content'; + +export interface LogsOverviewProps { + dependencies: LogsOverviewDependencies; + documentFilters?: QueryDslQueryContainer[]; + logsSource?: LogsSourceConfiguration; + timeRange: { + start: string; + end: string; + }; +} + +export type LogsOverviewDependencies = LogCategoriesDependencies & { + logsDataAccess: LogsDataAccessPluginStart; +}; + +export const LogsOverview: React.FC = React.memo( + ({ + dependencies, + documentFilters = defaultDocumentFilters, + logsSource = defaultLogsSource, + timeRange, + }) => { + const normalizedLogsSource = useAsync( + () => normalizeLogsSource({ logsDataAccess: dependencies.logsDataAccess })(logsSource), + [dependencies.logsDataAccess, logsSource] + ); + + if (normalizedLogsSource.loading) { + return ; + } + + if (normalizedLogsSource.error != null || normalizedLogsSource.value == null) { + return ; + } + + return ( + + ); + } +); + +const defaultDocumentFilters: QueryDslQueryContainer[] = []; + +const defaultLogsSource: LogsSourceConfiguration = { type: 'shared_setting' }; diff --git a/x-pack/packages/observability/logs_overview/src/components/logs_overview/logs_overview_error_content.tsx b/x-pack/packages/observability/logs_overview/src/components/logs_overview/logs_overview_error_content.tsx new file mode 100644 index 0000000000000..73586756bb908 --- /dev/null +++ b/x-pack/packages/observability/logs_overview/src/components/logs_overview/logs_overview_error_content.tsx @@ -0,0 +1,41 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { EuiCodeBlock, EuiEmptyPrompt } from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; +import React from 'react'; + +export interface LogsOverviewErrorContentProps { + error?: Error; +} + +export const LogsOverviewErrorContent: React.FC = ({ error }) => { + return ( + {logsOverviewErrorTitle}} + body={ + +

    {error?.stack ?? error?.toString() ?? unknownErrorDescription}

    +
    + } + layout="vertical" + /> + ); +}; + +const logsOverviewErrorTitle = i18n.translate('xpack.observabilityLogsOverview.errorTitle', { + defaultMessage: 'Error', +}); + +const unknownErrorDescription = i18n.translate( + 'xpack.observabilityLogsOverview.unknownErrorDescription', + { + defaultMessage: 'An unspecified error occurred.', + } +); diff --git a/x-pack/packages/observability/logs_overview/src/components/logs_overview/logs_overview_loading_content.tsx b/x-pack/packages/observability/logs_overview/src/components/logs_overview/logs_overview_loading_content.tsx new file mode 100644 index 0000000000000..7645fdb90f0ac --- /dev/null +++ b/x-pack/packages/observability/logs_overview/src/components/logs_overview/logs_overview_loading_content.tsx @@ -0,0 +1,23 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { EuiEmptyPrompt, EuiLoadingSpinner } from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; +import React from 'react'; + +export const LogsOverviewLoadingContent: React.FC = ({}) => { + return ( + } + title={

    {logsOverviewLoadingTitle}

    } + /> + ); +}; + +const logsOverviewLoadingTitle = i18n.translate('xpack.observabilityLogsOverview.loadingTitle', { + defaultMessage: 'Loading', +}); diff --git a/x-pack/packages/observability/logs_overview/src/services/categorize_logs_service/categorize_documents.ts b/x-pack/packages/observability/logs_overview/src/services/categorize_logs_service/categorize_documents.ts new file mode 100644 index 0000000000000..7260efe63d435 --- /dev/null +++ b/x-pack/packages/observability/logs_overview/src/services/categorize_logs_service/categorize_documents.ts @@ -0,0 +1,282 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { ISearchGeneric } from '@kbn/search-types'; +import { lastValueFrom } from 'rxjs'; +import { fromPromise } from 'xstate5'; +import { createRandomSamplerWrapper } from '@kbn/ml-random-sampler-utils'; +import { z } from '@kbn/zod'; +import { LogCategorizationParams } from './types'; +import { createCategorizationRequestParams } from './queries'; +import { LogCategory, LogCategoryChange } from '../../types'; + +// the fraction of a category's histogram below which the category is considered rare +const rarityThreshold = 0.2; +const maxCategoriesCount = 1000; + +export const categorizeDocuments = ({ search }: { search: ISearchGeneric }) => + fromPromise< + { + categories: LogCategory[]; + hasReachedLimit: boolean; + }, + LogCategorizationParams & { + samplingProbability: number; + ignoredCategoryTerms: string[]; + minDocsPerCategory: number; + } + >( + async ({ + input: { + index, + endTimestamp, + startTimestamp, + timeField, + messageField, + samplingProbability, + ignoredCategoryTerms, + documentFilters = [], + minDocsPerCategory, + }, + signal, + }) => { + const randomSampler = createRandomSamplerWrapper({ + probability: samplingProbability, + seed: 1, + }); + + const requestParams = createCategorizationRequestParams({ + index, + timeField, + messageField, + startTimestamp, + endTimestamp, + randomSampler, + additionalFilters: documentFilters, + ignoredCategoryTerms, + minDocsPerCategory, + maxCategoriesCount, + }); + + const { rawResponse } = await lastValueFrom( + search({ params: requestParams }, { abortSignal: signal }) + ); + + if (rawResponse.aggregations == null) { + throw new Error('No aggregations found in large categories response'); + } + + const logCategoriesAggResult = randomSampler.unwrap(rawResponse.aggregations); + + if (!('categories' in logCategoriesAggResult)) { + throw new Error('No categorization aggregation found in large categories response'); + } + + const logCategories = + (logCategoriesAggResult.categories.buckets as unknown[]).map(mapCategoryBucket) ?? []; + + return { + categories: logCategories, + hasReachedLimit: logCategories.length >= maxCategoriesCount, + }; + } + ); + +const mapCategoryBucket = (bucket: any): LogCategory => + esCategoryBucketSchema + .transform((parsedBucket) => ({ + change: mapChangePoint(parsedBucket), + documentCount: parsedBucket.doc_count, + histogram: parsedBucket.histogram, + terms: parsedBucket.key, + })) + .parse(bucket); + +const mapChangePoint = ({ change, histogram }: EsCategoryBucket): LogCategoryChange => { + switch (change.type) { + case 'stationary': + if (isRareInHistogram(histogram)) { + return { + type: 'rare', + timestamp: findFirstNonZeroBucket(histogram)?.timestamp ?? histogram[0].timestamp, + }; + } else { + return { + type: 'none', + }; + } + case 'dip': + case 'spike': + return { + type: change.type, + timestamp: change.bucket.key, + }; + case 'step_change': + return { + type: 'step', + timestamp: change.bucket.key, + }; + case 'distribution_change': + return { + type: 'distribution', + timestamp: change.bucket.key, + }; + case 'trend_change': + return { + type: 'trend', + timestamp: change.bucket.key, + correlationCoefficient: change.details.r_value, + }; + case 'unknown': + return { + type: 'unknown', + rawChange: change.rawChange, + }; + case 'non_stationary': + default: + return { + type: 'other', + }; + } +}; + +/** + * The official types are lacking the change_point aggregation + */ +const esChangePointBucketSchema = z.object({ + key: z.string().datetime(), + doc_count: z.number(), +}); + +const esChangePointDetailsSchema = z.object({ + p_value: z.number(), +}); + +const esChangePointCorrelationSchema = esChangePointDetailsSchema.extend({ + r_value: z.number(), +}); + +const esChangePointSchema = z.union([ + z + .object({ + bucket: esChangePointBucketSchema, + type: z.object({ + dip: esChangePointDetailsSchema, + }), + }) + .transform(({ bucket, type: { dip: details } }) => ({ + type: 'dip' as const, + bucket, + details, + })), + z + .object({ + bucket: esChangePointBucketSchema, + type: z.object({ + spike: esChangePointDetailsSchema, + }), + }) + .transform(({ bucket, type: { spike: details } }) => ({ + type: 'spike' as const, + bucket, + details, + })), + z + .object({ + bucket: esChangePointBucketSchema, + type: z.object({ + step_change: esChangePointDetailsSchema, + }), + }) + .transform(({ bucket, type: { step_change: details } }) => ({ + type: 'step_change' as const, + bucket, + details, + })), + z + .object({ + bucket: esChangePointBucketSchema, + type: z.object({ + trend_change: esChangePointCorrelationSchema, + }), + }) + .transform(({ bucket, type: { trend_change: details } }) => ({ + type: 'trend_change' as const, + bucket, + details, + })), + z + .object({ + bucket: esChangePointBucketSchema, + type: z.object({ + distribution_change: esChangePointDetailsSchema, + }), + }) + .transform(({ bucket, type: { distribution_change: details } }) => ({ + type: 'distribution_change' as const, + bucket, + details, + })), + z + .object({ + type: z.object({ + non_stationary: esChangePointCorrelationSchema.extend({ + trend: z.enum(['increasing', 'decreasing']), + }), + }), + }) + .transform(({ type: { non_stationary: details } }) => ({ + type: 'non_stationary' as const, + details, + })), + z + .object({ + type: z.object({ + stationary: z.object({}), + }), + }) + .transform(() => ({ type: 'stationary' as const })), + z + .object({ + type: z.object({}), + }) + .transform((value) => ({ type: 'unknown' as const, rawChange: JSON.stringify(value) })), +]); + +const esHistogramSchema = z + .object({ + buckets: z.array( + z + .object({ + key_as_string: z.string(), + doc_count: z.number(), + }) + .transform((bucket) => ({ + timestamp: bucket.key_as_string, + documentCount: bucket.doc_count, + })) + ), + }) + .transform(({ buckets }) => buckets); + +type EsHistogram = z.output; + +const esCategoryBucketSchema = z.object({ + key: z.string(), + doc_count: z.number(), + change: esChangePointSchema, + histogram: esHistogramSchema, +}); + +type EsCategoryBucket = z.output; + +const isRareInHistogram = (histogram: EsHistogram): boolean => + histogram.filter((bucket) => bucket.documentCount > 0).length < + histogram.length * rarityThreshold; + +const findFirstNonZeroBucket = (histogram: EsHistogram) => + histogram.find((bucket) => bucket.documentCount > 0); diff --git a/x-pack/packages/observability/logs_overview/src/services/categorize_logs_service/categorize_logs_service.ts b/x-pack/packages/observability/logs_overview/src/services/categorize_logs_service/categorize_logs_service.ts new file mode 100644 index 0000000000000..deeb758d2d737 --- /dev/null +++ b/x-pack/packages/observability/logs_overview/src/services/categorize_logs_service/categorize_logs_service.ts @@ -0,0 +1,250 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { MachineImplementationsFrom, assign, setup } from 'xstate5'; +import { LogCategory } from '../../types'; +import { getPlaceholderFor } from '../../utils/xstate5_utils'; +import { categorizeDocuments } from './categorize_documents'; +import { countDocuments } from './count_documents'; +import { CategorizeLogsServiceDependencies, LogCategorizationParams } from './types'; + +export const categorizeLogsService = setup({ + types: { + input: {} as LogCategorizationParams, + output: {} as { + categories: LogCategory[]; + documentCount: number; + hasReachedLimit: boolean; + samplingProbability: number; + }, + context: {} as { + categories: LogCategory[]; + documentCount: number; + error?: Error; + hasReachedLimit: boolean; + parameters: LogCategorizationParams; + samplingProbability: number; + }, + events: {} as { + type: 'cancel'; + }, + }, + actors: { + countDocuments: getPlaceholderFor(countDocuments), + categorizeDocuments: getPlaceholderFor(categorizeDocuments), + }, + actions: { + storeError: assign((_, params: { error: unknown }) => ({ + error: params.error instanceof Error ? params.error : new Error(String(params.error)), + })), + storeCategories: assign( + ({ context }, params: { categories: LogCategory[]; hasReachedLimit: boolean }) => ({ + categories: [...context.categories, ...params.categories], + hasReachedLimit: params.hasReachedLimit, + }) + ), + storeDocumentCount: assign( + (_, params: { documentCount: number; samplingProbability: number }) => ({ + documentCount: params.documentCount, + samplingProbability: params.samplingProbability, + }) + ), + }, + guards: { + hasTooFewDocuments: (_guardArgs, params: { documentCount: number }) => params.documentCount < 1, + requiresSampling: (_guardArgs, params: { samplingProbability: number }) => + params.samplingProbability < 1, + }, +}).createMachine({ + /** @xstate-layout N4IgpgJg5mDOIC5QGMCGAXMUD2AnAlgF5gAy2UsAdMtgK4B26+9UAItsrQLZiOwDEEbPTCVmAN2wBrUWkw4CxMhWp1GzNh2690sBBI4Z8wgNoAGALrmLiUAAdssfE2G2QAD0QBmMwA5KACy+AQFmob4AjABMwQBsADQgAJ6IkYEAnJkA7FmxZlERmQGxAL4liXJYeESk5FQ0DEws7Jw8fILCogYy1BhVirUqDerNWm26+vSScsb01iYRNkggDk4u9G6eCD7+QSFhftFxiSkIvgCsWZSxEVlRsbFZ52Zm515lFX0KNcr1ak2aVo6ARCERiKbSWRfapKOqqRoaFraPiTaZGUyWExRJb2RzOWabbx+QLBULhI7FE7eWL+F45GnRPIRZkfECVb6wob-RFjYH8MC4XB4Sh2AA2GAAZnguL15DDBn8EaMgSiDDMMVZLG5VvjXMstjsSftyTFKclEOdzgFKF5zukvA8zBFnl50udWez5b94SNAcjdPw0PRkGBRdZtXj1oTtsS9mTDqaEuaEBF8udKFkIr5fK6olkzOksgEPdCBt6JWB0MgABYaADKqC4YsgAGFS-g4B0wd0oXKBg2m6LW+24OHljqo-rEMzbpQos8-K7fC9CknTrF0rEbbb0oVMoWIgF3eU2e3OVQK1XaywB82IG2+x2BAKhbgReL0FLcDLPf3G3eH36J8x1xNYCSnFNmSuecXhzdJlydTcqQQLJfHSOc0PyLJN3SMxYiPEtH3PShLxret-yHe8RwEIMQzDLVx0jcDQC2GdoIXOCENXZDsyiOcAiiKJ0iiPDLi8V1CKA4jSOvKAACUwC4VBmA0QDvk7UEughHpfxqBSlJUlg1OqUcGNA3UNggrMs347IjzdaIvGQwSvECXI8k3Z43gEiJJI5BUSMrMiWH05T6FU6j+UFYUxUlaVZSksBQsMqBjIIUycRWJi9RY6dIn8KIAjsu1zkc5CAmiG1fBiaIzB8B0QmPT4iICmSNGS8KjMi2jQxArKwJyjw8pswriocqInOTLwIi3ASD1yQpswCd5WXobAIDgNxdPPCMBss3KEAAWjXRBDvTfcLsu9Jlr8r04WGAEkXGeBGL26MBOQzIt2ut4cwmirCt8W6yzhNqbwo4dH0216LOjTMIjnBdYhK1DYgdHjihtZbUIdWIXJuYGflBoLZI6iKoZe8zJwOw9KtGt1kbuTcsmQrwi0oeCQjzZ5blwt1Cek5TKN22GIIKZbAgKC45pyLyeLwtz4Kyabs1QgWAs0kXqaGhBxdcnzpaE2XXmch0MORmaBJeLwjbKMogA */ + id: 'categorizeLogs', + context: ({ input }) => ({ + categories: [], + documentCount: 0, + hasReachedLimit: false, + parameters: input, + samplingProbability: 1, + }), + initial: 'countingDocuments', + states: { + countingDocuments: { + invoke: { + src: 'countDocuments', + input: ({ context }) => context.parameters, + onDone: [ + { + target: 'done', + guard: { + type: 'hasTooFewDocuments', + params: ({ event }) => event.output, + }, + actions: [ + { + type: 'storeDocumentCount', + params: ({ event }) => event.output, + }, + ], + }, + { + target: 'fetchingSampledCategories', + guard: { + type: 'requiresSampling', + params: ({ event }) => event.output, + }, + actions: [ + { + type: 'storeDocumentCount', + params: ({ event }) => event.output, + }, + ], + }, + { + target: 'fetchingRemainingCategories', + actions: [ + { + type: 'storeDocumentCount', + params: ({ event }) => event.output, + }, + ], + }, + ], + onError: { + target: 'failed', + actions: [ + { + type: 'storeError', + params: ({ event }) => ({ error: event.error }), + }, + ], + }, + }, + + on: { + cancel: { + target: 'failed', + actions: [ + { + type: 'storeError', + params: () => ({ error: new Error('Counting cancelled') }), + }, + ], + }, + }, + }, + + fetchingSampledCategories: { + invoke: { + src: 'categorizeDocuments', + id: 'categorizeSampledCategories', + input: ({ context }) => ({ + ...context.parameters, + samplingProbability: context.samplingProbability, + ignoredCategoryTerms: [], + minDocsPerCategory: 10, + }), + onDone: { + target: 'fetchingRemainingCategories', + actions: [ + { + type: 'storeCategories', + params: ({ event }) => event.output, + }, + ], + }, + onError: { + target: 'failed', + actions: [ + { + type: 'storeError', + params: ({ event }) => ({ error: event.error }), + }, + ], + }, + }, + + on: { + cancel: { + target: 'failed', + actions: [ + { + type: 'storeError', + params: () => ({ error: new Error('Categorization cancelled') }), + }, + ], + }, + }, + }, + + fetchingRemainingCategories: { + invoke: { + src: 'categorizeDocuments', + id: 'categorizeRemainingCategories', + input: ({ context }) => ({ + ...context.parameters, + samplingProbability: 1, + ignoredCategoryTerms: context.categories.map((category) => category.terms), + minDocsPerCategory: 0, + }), + onDone: { + target: 'done', + actions: [ + { + type: 'storeCategories', + params: ({ event }) => event.output, + }, + ], + }, + onError: { + target: 'failed', + actions: [ + { + type: 'storeError', + params: ({ event }) => ({ error: event.error }), + }, + ], + }, + }, + + on: { + cancel: { + target: 'failed', + actions: [ + { + type: 'storeError', + params: () => ({ error: new Error('Categorization cancelled') }), + }, + ], + }, + }, + }, + + failed: { + type: 'final', + }, + + done: { + type: 'final', + }, + }, + output: ({ context }) => ({ + categories: context.categories, + documentCount: context.documentCount, + hasReachedLimit: context.hasReachedLimit, + samplingProbability: context.samplingProbability, + }), +}); + +export const createCategorizeLogsServiceImplementations = ({ + search, +}: CategorizeLogsServiceDependencies): MachineImplementationsFrom< + typeof categorizeLogsService +> => ({ + actors: { + categorizeDocuments: categorizeDocuments({ search }), + countDocuments: countDocuments({ search }), + }, +}); diff --git a/x-pack/packages/observability/logs_overview/src/services/categorize_logs_service/count_documents.ts b/x-pack/packages/observability/logs_overview/src/services/categorize_logs_service/count_documents.ts new file mode 100644 index 0000000000000..359f9ddac2bd8 --- /dev/null +++ b/x-pack/packages/observability/logs_overview/src/services/categorize_logs_service/count_documents.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 { getSampleProbability } from '@kbn/ml-random-sampler-utils'; +import { ISearchGeneric } from '@kbn/search-types'; +import { lastValueFrom } from 'rxjs'; +import { fromPromise } from 'xstate5'; +import { LogCategorizationParams } from './types'; +import { createCategorizationQuery } from './queries'; + +export const countDocuments = ({ search }: { search: ISearchGeneric }) => + fromPromise< + { + documentCount: number; + samplingProbability: number; + }, + LogCategorizationParams + >( + async ({ + input: { index, endTimestamp, startTimestamp, timeField, messageField, documentFilters }, + signal, + }) => { + const { rawResponse: totalHitsResponse } = await lastValueFrom( + search( + { + params: { + index, + size: 0, + track_total_hits: true, + query: createCategorizationQuery({ + messageField, + timeField, + startTimestamp, + endTimestamp, + additionalFilters: documentFilters, + }), + }, + }, + { abortSignal: signal } + ) + ); + + const documentCount = + totalHitsResponse.hits.total == null + ? 0 + : typeof totalHitsResponse.hits.total === 'number' + ? totalHitsResponse.hits.total + : totalHitsResponse.hits.total.value; + const samplingProbability = getSampleProbability(documentCount); + + return { + documentCount, + samplingProbability, + }; + } + ); diff --git a/x-pack/packages/observability/logs_overview/src/services/categorize_logs_service/index.ts b/x-pack/packages/observability/logs_overview/src/services/categorize_logs_service/index.ts new file mode 100644 index 0000000000000..149359b7d2015 --- /dev/null +++ b/x-pack/packages/observability/logs_overview/src/services/categorize_logs_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 './categorize_logs_service'; diff --git a/x-pack/packages/observability/logs_overview/src/services/categorize_logs_service/queries.ts b/x-pack/packages/observability/logs_overview/src/services/categorize_logs_service/queries.ts new file mode 100644 index 0000000000000..aef12da303bcc --- /dev/null +++ b/x-pack/packages/observability/logs_overview/src/services/categorize_logs_service/queries.ts @@ -0,0 +1,151 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { QueryDslQueryContainer } from '@elastic/elasticsearch/lib/api/types'; +import { calculateAuto } from '@kbn/calculate-auto'; +import { RandomSamplerWrapper } from '@kbn/ml-random-sampler-utils'; +import moment from 'moment'; + +const isoTimestampFormat = "YYYY-MM-DD'T'HH:mm:ss.SSS'Z'"; + +export const createCategorizationQuery = ({ + messageField, + timeField, + startTimestamp, + endTimestamp, + additionalFilters = [], + ignoredCategoryTerms = [], +}: { + messageField: string; + timeField: string; + startTimestamp: string; + endTimestamp: string; + additionalFilters?: QueryDslQueryContainer[]; + ignoredCategoryTerms?: string[]; +}): QueryDslQueryContainer => { + return { + bool: { + filter: [ + { + exists: { + field: messageField, + }, + }, + { + range: { + [timeField]: { + gte: startTimestamp, + lte: endTimestamp, + format: 'strict_date_time', + }, + }, + }, + ...additionalFilters, + ], + must_not: ignoredCategoryTerms.map(createCategoryQuery(messageField)), + }, + }; +}; + +export const createCategorizationRequestParams = ({ + index, + timeField, + messageField, + startTimestamp, + endTimestamp, + randomSampler, + minDocsPerCategory = 0, + additionalFilters = [], + ignoredCategoryTerms = [], + maxCategoriesCount = 1000, +}: { + startTimestamp: string; + endTimestamp: string; + index: string; + timeField: string; + messageField: string; + randomSampler: RandomSamplerWrapper; + minDocsPerCategory?: number; + additionalFilters?: QueryDslQueryContainer[]; + ignoredCategoryTerms?: string[]; + maxCategoriesCount?: number; +}) => { + const startMoment = moment(startTimestamp, isoTimestampFormat); + const endMoment = moment(endTimestamp, isoTimestampFormat); + const fixedIntervalDuration = calculateAuto.atLeast( + 24, + moment.duration(endMoment.diff(startMoment)) + ); + const fixedIntervalSize = `${Math.ceil(fixedIntervalDuration?.asMinutes() ?? 1)}m`; + + return { + index, + size: 0, + track_total_hits: false, + query: createCategorizationQuery({ + messageField, + timeField, + startTimestamp, + endTimestamp, + additionalFilters, + ignoredCategoryTerms, + }), + aggs: randomSampler.wrap({ + histogram: { + date_histogram: { + field: timeField, + fixed_interval: fixedIntervalSize, + extended_bounds: { + min: startTimestamp, + max: endTimestamp, + }, + }, + }, + categories: { + categorize_text: { + field: messageField, + size: maxCategoriesCount, + categorization_analyzer: { + tokenizer: 'standard', + }, + ...(minDocsPerCategory > 0 ? { min_doc_count: minDocsPerCategory } : {}), + }, + aggs: { + histogram: { + date_histogram: { + field: timeField, + fixed_interval: fixedIntervalSize, + extended_bounds: { + min: startTimestamp, + max: endTimestamp, + }, + }, + }, + change: { + // @ts-expect-error the official types don't support the change_point aggregation + change_point: { + buckets_path: 'histogram>_count', + }, + }, + }, + }, + }), + }; +}; + +export const createCategoryQuery = + (messageField: string) => + (categoryTerms: string): QueryDslQueryContainer => ({ + match: { + [messageField]: { + query: categoryTerms, + operator: 'AND' as const, + fuzziness: 0, + auto_generate_synonyms_phrase_query: false, + }, + }, + }); diff --git a/x-pack/packages/observability/logs_overview/src/services/categorize_logs_service/types.ts b/x-pack/packages/observability/logs_overview/src/services/categorize_logs_service/types.ts new file mode 100644 index 0000000000000..e094317a98d62 --- /dev/null +++ b/x-pack/packages/observability/logs_overview/src/services/categorize_logs_service/types.ts @@ -0,0 +1,21 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ +import { QueryDslQueryContainer } from '@elastic/elasticsearch/lib/api/types'; +import { ISearchGeneric } from '@kbn/search-types'; + +export interface CategorizeLogsServiceDependencies { + search: ISearchGeneric; +} + +export interface LogCategorizationParams { + documentFilters: QueryDslQueryContainer[]; + endTimestamp: string; + index: string; + messageField: string; + startTimestamp: string; + timeField: string; +} diff --git a/x-pack/packages/observability/logs_overview/src/types.ts b/x-pack/packages/observability/logs_overview/src/types.ts new file mode 100644 index 0000000000000..4c3d27eca7e7c --- /dev/null +++ b/x-pack/packages/observability/logs_overview/src/types.ts @@ -0,0 +1,74 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +export interface LogCategory { + change: LogCategoryChange; + documentCount: number; + histogram: LogCategoryHistogramBucket[]; + terms: string; +} + +export type LogCategoryChange = + | LogCategoryNoChange + | LogCategoryRareChange + | LogCategorySpikeChange + | LogCategoryDipChange + | LogCategoryStepChange + | LogCategoryDistributionChange + | LogCategoryTrendChange + | LogCategoryOtherChange + | LogCategoryUnknownChange; + +export interface LogCategoryNoChange { + type: 'none'; +} + +export interface LogCategoryRareChange { + type: 'rare'; + timestamp: string; +} + +export interface LogCategorySpikeChange { + type: 'spike'; + timestamp: string; +} + +export interface LogCategoryDipChange { + type: 'dip'; + timestamp: string; +} + +export interface LogCategoryStepChange { + type: 'step'; + timestamp: string; +} + +export interface LogCategoryTrendChange { + type: 'trend'; + timestamp: string; + correlationCoefficient: number; +} + +export interface LogCategoryDistributionChange { + type: 'distribution'; + timestamp: string; +} + +export interface LogCategoryOtherChange { + type: 'other'; + timestamp?: string; +} + +export interface LogCategoryUnknownChange { + type: 'unknown'; + rawChange: string; +} + +export interface LogCategoryHistogramBucket { + documentCount: number; + timestamp: string; +} diff --git a/x-pack/packages/observability/logs_overview/src/utils/logs_source.ts b/x-pack/packages/observability/logs_overview/src/utils/logs_source.ts new file mode 100644 index 0000000000000..0c8767c8702d4 --- /dev/null +++ b/x-pack/packages/observability/logs_overview/src/utils/logs_source.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 { type AbstractDataView } from '@kbn/data-views-plugin/common'; +import { LogsDataAccessPluginStart } from '@kbn/logs-data-access-plugin/public'; + +export type LogsSourceConfiguration = + | SharedSettingLogsSourceConfiguration + | IndexNameLogsSourceConfiguration + | DataViewLogsSourceConfiguration; + +export interface SharedSettingLogsSourceConfiguration { + type: 'shared_setting'; + timestampField?: string; + messageField?: string; +} + +export interface IndexNameLogsSourceConfiguration { + type: 'index_name'; + indexName: string; + timestampField: string; + messageField: string; +} + +export interface DataViewLogsSourceConfiguration { + type: 'data_view'; + dataView: AbstractDataView; + messageField?: string; +} + +export const normalizeLogsSource = + ({ logsDataAccess }: { logsDataAccess: LogsDataAccessPluginStart }) => + async (logsSource: LogsSourceConfiguration): Promise => { + switch (logsSource.type) { + case 'index_name': + return logsSource; + case 'shared_setting': + const logSourcesFromSharedSettings = + await logsDataAccess.services.logSourcesService.getLogSources(); + return { + type: 'index_name', + indexName: logSourcesFromSharedSettings + .map((logSource) => logSource.indexPattern) + .join(','), + timestampField: logsSource.timestampField ?? '@timestamp', + messageField: logsSource.messageField ?? 'message', + }; + case 'data_view': + return { + type: 'index_name', + indexName: logsSource.dataView.getIndexPattern(), + timestampField: logsSource.dataView.timeFieldName ?? '@timestamp', + messageField: logsSource.messageField ?? 'message', + }; + } + }; diff --git a/x-pack/packages/observability/logs_overview/src/utils/xstate5_utils.ts b/x-pack/packages/observability/logs_overview/src/utils/xstate5_utils.ts new file mode 100644 index 0000000000000..3df0bf4ea3988 --- /dev/null +++ b/x-pack/packages/observability/logs_overview/src/utils/xstate5_utils.ts @@ -0,0 +1,13 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +export const getPlaceholderFor = any>( + implementationFactory: ImplementationFactory +): ReturnType => + (() => { + throw new Error('Not implemented'); + }) as ReturnType; diff --git a/x-pack/packages/observability/logs_overview/tsconfig.json b/x-pack/packages/observability/logs_overview/tsconfig.json new file mode 100644 index 0000000000000..886062ae8855f --- /dev/null +++ b/x-pack/packages/observability/logs_overview/tsconfig.json @@ -0,0 +1,39 @@ +{ + "extends": "../../../../tsconfig.base.json", + "compilerOptions": { + "outDir": "target/types", + "types": [ + "jest", + "node", + "react", + "@kbn/ambient-ui-types", + "@kbn/ambient-storybook-types", + "@emotion/react/types/css-prop" + ] + }, + "include": [ + "**/*.ts", + "**/*.tsx", + ], + "exclude": [ + "target/**/*" + ], + "kbn_references": [ + "@kbn/data-views-plugin", + "@kbn/i18n", + "@kbn/search-types", + "@kbn/xstate-utils", + "@kbn/core-ui-settings-browser", + "@kbn/i18n-react", + "@kbn/charts-plugin", + "@kbn/utility-types", + "@kbn/logs-data-access-plugin", + "@kbn/ml-random-sampler-utils", + "@kbn/zod", + "@kbn/calculate-auto", + "@kbn/discover-plugin", + "@kbn/es-query", + "@kbn/router-utils", + "@kbn/share-plugin", + ] +} diff --git a/x-pack/packages/security-solution/distribution_bar/src/distribution_bar.stories.tsx b/x-pack/packages/security-solution/distribution_bar/src/distribution_bar.stories.tsx index 90b6887636c8a..c1b292c3f08cc 100644 --- a/x-pack/packages/security-solution/distribution_bar/src/distribution_bar.stories.tsx +++ b/x-pack/packages/security-solution/distribution_bar/src/distribution_bar.stories.tsx @@ -70,6 +70,14 @@ export const DistributionBar = () => { , + + +

    {'Hide last tooltip'}

    +
    + + + +
    ,

    {'Empty state'}

    diff --git a/x-pack/packages/security-solution/distribution_bar/src/distribution_bar.test.tsx b/x-pack/packages/security-solution/distribution_bar/src/distribution_bar.test.tsx index d4bdf4c20f133..e83b66e5e01e7 100644 --- a/x-pack/packages/security-solution/distribution_bar/src/distribution_bar.test.tsx +++ b/x-pack/packages/security-solution/distribution_bar/src/distribution_bar.test.tsx @@ -79,5 +79,67 @@ describe('DistributionBar', () => { }); }); + it('should render last tooltip by default', () => { + const stats = [ + { + key: 'low', + count: 9, + color: 'green', + }, + { + key: 'medium', + count: 90, + color: 'red', + }, + { + key: 'high', + count: 900, + color: 'red', + }, + ]; + + const { container } = render( + + ); + expect(container).toBeInTheDocument(); + const parts = container.querySelectorAll(`[classname*="distribution_bar--tooltip"]`); + parts.forEach((part, index) => { + if (index < parts.length - 1) { + expect(part).toHaveStyle({ opacity: 0 }); + } else { + expect(part).toHaveStyle({ opacity: 1 }); + } + }); + }); + + it('should not render last tooltip when hideLastTooltip is true', () => { + const stats = [ + { + key: 'low', + count: 9, + color: 'green', + }, + { + key: 'medium', + count: 90, + color: 'red', + }, + { + key: 'high', + count: 900, + color: 'red', + }, + ]; + + const { container } = render( + + ); + expect(container).toBeInTheDocument(); + const parts = container.querySelectorAll(`[classname*="distribution_bar--tooltip"]`); + parts.forEach((part) => { + expect(part).toHaveStyle({ opacity: 0 }); + }); + }); + // todo: test tooltip visibility logic }); diff --git a/x-pack/packages/security-solution/distribution_bar/src/distribution_bar.tsx b/x-pack/packages/security-solution/distribution_bar/src/distribution_bar.tsx index 28d8ca4a8a148..5b06292813ccd 100644 --- a/x-pack/packages/security-solution/distribution_bar/src/distribution_bar.tsx +++ b/x-pack/packages/security-solution/distribution_bar/src/distribution_bar.tsx @@ -13,6 +13,8 @@ import { css } from '@emotion/react'; export interface DistributionBarProps { /** distribution data points */ stats: Array<{ key: string; count: number; color: string; label?: React.ReactNode }>; + /** hide the label above the bar at first render */ + hideLastTooltip?: boolean; /** data-test-subj used for querying the component in tests */ ['data-test-subj']?: string; } @@ -136,18 +138,21 @@ export const DistributionBar: React.FC = React.memo(functi props ) { const styles = useStyles(); - const { stats, 'data-test-subj': dataTestSubj } = props; + const { stats, 'data-test-subj': dataTestSubj, hideLastTooltip } = props; const parts = stats.map((stat) => { const partStyle = [ styles.part.base, styles.part.tick, styles.part.hover, - styles.part.lastTooltip, css` background-color: ${stat.color}; flex: ${stat.count}; `, ]; + if (!hideLastTooltip) { + partStyle.push(styles.part.lastTooltip); + } + const prettyNumber = numeral(stat.count).format('0,0a'); return ( diff --git a/x-pack/packages/security/api_key_management/src/components/api_key_badge.tsx b/x-pack/packages/security/api_key_management/src/components/api_key_badge.tsx index d8fb2822a1e96..c0c1c2a1e823d 100644 --- a/x-pack/packages/security/api_key_management/src/components/api_key_badge.tsx +++ b/x-pack/packages/security/api_key_management/src/components/api_key_badge.tsx @@ -5,8 +5,10 @@ * 2.0. */ -import { EuiToolTip, EuiBadge } from '@elastic/eui'; -import React, { FunctionComponent } from 'react'; +import { EuiBadge, EuiToolTip } from '@elastic/eui'; +import type { FunctionComponent } from 'react'; +import React from 'react'; + import { FormattedMessage } from '@kbn/i18n-react'; export interface ApiKeyBadgeProps { diff --git a/x-pack/packages/security/api_key_management/src/components/api_key_created_callout.tsx b/x-pack/packages/security/api_key_management/src/components/api_key_created_callout.tsx index 1f2f0201d5a48..daea08b51bf96 100644 --- a/x-pack/packages/security/api_key_management/src/components/api_key_created_callout.tsx +++ b/x-pack/packages/security/api_key_management/src/components/api_key_created_callout.tsx @@ -6,10 +6,13 @@ */ import { EuiCallOut } from '@elastic/eui'; +import type { FunctionComponent } from 'react'; +import React from 'react'; + import { i18n } from '@kbn/i18n'; -import React, { FunctionComponent } from 'react'; import { FormattedMessage } from '@kbn/i18n-react'; -import { CreateAPIKeyResult } from './api_keys_api_client'; + +import type { CreateAPIKeyResult } from './api_keys_api_client'; import { SelectableTokenField } from './token_field'; export interface ApiKeyCreatedCalloutProps { diff --git a/x-pack/packages/security/api_key_management/src/components/api_key_flyout.tsx b/x-pack/packages/security/api_key_management/src/components/api_key_flyout.tsx index 4a8fa74095957..1edaca0761c47 100644 --- a/x-pack/packages/security/api_key_management/src/components/api_key_flyout.tsx +++ b/x-pack/packages/security/api_key_management/src/components/api_key_flyout.tsx @@ -5,7 +5,7 @@ * 2.0. */ -import { ExclusiveUnion, htmlIdGenerator } from '@elastic/eui'; +import type { ExclusiveUnion } from '@elastic/eui'; import { EuiButton, EuiButtonEmpty, @@ -27,12 +27,13 @@ import { EuiSwitch, EuiText, EuiTitle, + htmlIdGenerator, useEuiTheme, } from '@elastic/eui'; import { Form, FormikProvider, useFormik } from 'formik'; import moment from 'moment-timezone'; -import { FunctionComponent, useRef } from 'react'; -import React, { useEffect, useState } from 'react'; +import type { FunctionComponent } from 'react'; +import React, { useEffect, useRef, useState } from 'react'; import useAsyncFn from 'react-use/lib/useAsyncFn'; import { CodeEditorField } from '@kbn/code-editor'; @@ -41,10 +42,13 @@ import { i18n } from '@kbn/i18n'; import { FormattedDate, FormattedMessage } from '@kbn/i18n-react'; import { useDarkMode, useKibana } from '@kbn/kibana-react-plugin/public'; import type { KibanaServerError } from '@kbn/kibana-utils-plugin/public'; - -import { Role } from '@kbn/security-plugin-types-common'; import { FormField, FormRow } from '@kbn/security-form-components'; -import type { ApiKeyRoleDescriptors, CategorizedApiKey } from '@kbn/security-plugin-types-common'; +import type { + ApiKeyRoleDescriptors, + CategorizedApiKey, + Role, +} from '@kbn/security-plugin-types-common'; + import { ApiKeyBadge, ApiKeyStatus, TimeToolTip } from '.'; import { APIKeysAPIClient } from './api_keys_api_client'; import type { diff --git a/x-pack/packages/security/api_key_management/src/components/api_key_status.tsx b/x-pack/packages/security/api_key_management/src/components/api_key_status.tsx index 677544866e3cf..c45ad145d9e51 100644 --- a/x-pack/packages/security/api_key_management/src/components/api_key_status.tsx +++ b/x-pack/packages/security/api_key_management/src/components/api_key_status.tsx @@ -7,9 +7,12 @@ import { EuiHealth } from '@elastic/eui'; import moment from 'moment'; -import React, { FunctionComponent } from 'react'; +import type { FunctionComponent } from 'react'; +import React from 'react'; + import { FormattedMessage } from '@kbn/i18n-react'; -import { CategorizedApiKey } from '@kbn/security-plugin-types-common'; +import type { CategorizedApiKey } from '@kbn/security-plugin-types-common'; + import { TimeToolTip } from './time_tool_tip'; export type ApiKeyStatusProps = Pick; diff --git a/x-pack/packages/security/api_key_management/src/components/api_keys_api_client.ts b/x-pack/packages/security/api_key_management/src/components/api_keys_api_client.ts index 30cc9f214ebf5..11f02ffe98782 100644 --- a/x-pack/packages/security/api_key_management/src/components/api_keys_api_client.ts +++ b/x-pack/packages/security/api_key_management/src/components/api_keys_api_client.ts @@ -5,9 +5,15 @@ * 2.0. */ +import type { Criteria } from '@elastic/eui'; import type { QueryContainer } from '@elastic/eui/src/components/search_bar/query/ast_to_es_query_dsl'; import type { HttpStart } from '@kbn/core/public'; +import type { + ApiKeyToInvalidate, + CategorizedApiKey, + QueryApiKeyResult, +} from '@kbn/security-plugin-types-common'; import type { CreateAPIKeyParams, CreateAPIKeyResult, @@ -15,13 +21,6 @@ import type { UpdateAPIKeyResult, } from '@kbn/security-plugin-types-server'; -import type { - ApiKeyToInvalidate, - CategorizedApiKey, - QueryApiKeyResult, -} from '@kbn/security-plugin-types-common'; -import type { Criteria } from '@elastic/eui'; - export type { CreateAPIKeyParams, CreateAPIKeyResult, UpdateAPIKeyParams, UpdateAPIKeyResult }; export interface QueryFilters { diff --git a/x-pack/packages/security/api_key_management/src/components/time_tool_tip.tsx b/x-pack/packages/security/api_key_management/src/components/time_tool_tip.tsx index dacfd7e0c1344..0864c6b7bf1d2 100644 --- a/x-pack/packages/security/api_key_management/src/components/time_tool_tip.tsx +++ b/x-pack/packages/security/api_key_management/src/components/time_tool_tip.tsx @@ -7,8 +7,8 @@ import { EuiToolTip } from '@elastic/eui'; import moment from 'moment'; +import type { FunctionComponent } from 'react'; import React from 'react'; -import { FunctionComponent } from 'react'; export interface TimeToolTipProps { timestamp: number; diff --git a/x-pack/packages/security/authorization_core/src/privileges/privileges.ts b/x-pack/packages/security/authorization_core/src/privileges/privileges.ts index 9fb8dd9f083e2..6b8acc4e4013a 100644 --- a/x-pack/packages/security/authorization_core/src/privileges/privileges.ts +++ b/x-pack/packages/security/authorization_core/src/privileges/privileges.ts @@ -12,8 +12,8 @@ import type { FeatureKibanaPrivilegesReference, } from '@kbn/features-plugin/common'; import type { FeaturesPluginSetup, KibanaFeature } from '@kbn/features-plugin/server'; - import type { SecurityLicense } from '@kbn/security-plugin-types-common'; + import { featurePrivilegeBuilderFactory } from './feature_privilege_builder'; import type { RawKibanaPrivileges } from './raw_kibana_privileges'; import type { Actions } from '../actions'; diff --git a/x-pack/packages/security/plugin_types_public/src/authorization/authorization_service.ts b/x-pack/packages/security/plugin_types_public/src/authorization/authorization_service.ts index f04acf8020b24..71e5c7360634f 100644 --- a/x-pack/packages/security/plugin_types_public/src/authorization/authorization_service.ts +++ b/x-pack/packages/security/plugin_types_public/src/authorization/authorization_service.ts @@ -5,8 +5,8 @@ * 2.0. */ -import type { RolesAPIClient } from '../roles'; import type { PrivilegesAPIClientPublicContract } from '../privileges'; +import type { RolesAPIClient } from '../roles'; export interface AuthorizationServiceSetup { /** diff --git a/x-pack/packages/security/plugin_types_public/src/plugin.ts b/x-pack/packages/security/plugin_types_public/src/plugin.ts index 06f3574388a36..4a3351494a631 100644 --- a/x-pack/packages/security/plugin_types_public/src/plugin.ts +++ b/x-pack/packages/security/plugin_types_public/src/plugin.ts @@ -6,6 +6,7 @@ */ import type { SecurityLicense } from '@kbn/security-plugin-types-common'; + import type { AuthenticationServiceSetup, AuthenticationServiceStart } from './authentication'; import type { AuthorizationServiceSetup, AuthorizationServiceStart } from './authorization'; import type { SecurityNavControlServiceStart } from './nav_control'; diff --git a/x-pack/packages/security/plugin_types_public/src/user_profile/user_profile_api_client.ts b/x-pack/packages/security/plugin_types_public/src/user_profile/user_profile_api_client.ts index eedf6e87a6483..862bcdccf942e 100644 --- a/x-pack/packages/security/plugin_types_public/src/user_profile/user_profile_api_client.ts +++ b/x-pack/packages/security/plugin_types_public/src/user_profile/user_profile_api_client.ts @@ -5,9 +5,10 @@ * 2.0. */ +import type { Observable } from 'rxjs'; + import type { CoreUserProfileDelegateContract } from '@kbn/core-user-profile-browser'; import type { UserProfileData } from '@kbn/core-user-profile-common'; -import type { Observable } from 'rxjs'; export type { GetUserProfileResponse, diff --git a/x-pack/packages/security/plugin_types_server/src/audit/audit_service.ts b/x-pack/packages/security/plugin_types_server/src/audit/audit_service.ts index e7b7f27b73b07..89524efb06339 100644 --- a/x-pack/packages/security/plugin_types_server/src/audit/audit_service.ts +++ b/x-pack/packages/security/plugin_types_server/src/audit/audit_service.ts @@ -6,7 +6,6 @@ */ import type { KibanaRequest } from '@kbn/core/server'; - import type { AuditLogger } from '@kbn/core-security-server'; export interface AuditServiceSetup { diff --git a/x-pack/packages/security/plugin_types_server/src/authentication/api_keys/api_keys.ts b/x-pack/packages/security/plugin_types_server/src/authentication/api_keys/api_keys.ts index c331802c7f693..f8d9f085a3246 100644 --- a/x-pack/packages/security/plugin_types_server/src/authentication/api_keys/api_keys.ts +++ b/x-pack/packages/security/plugin_types_server/src/authentication/api_keys/api_keys.ts @@ -6,7 +6,8 @@ */ import { schema } from '@kbn/config-schema'; -import { getKibanaRoleSchema, elasticsearchRoleSchema } from '../../authorization'; + +import { elasticsearchRoleSchema, getKibanaRoleSchema } from '../../authorization'; export const restApiKeySchema = schema.object({ type: schema.maybe(schema.literal('rest')), diff --git a/x-pack/packages/security/plugin_types_server/src/authentication/authentication_service.ts b/x-pack/packages/security/plugin_types_server/src/authentication/authentication_service.ts index 5d066bb6565ca..f5ee4ef7f25c1 100644 --- a/x-pack/packages/security/plugin_types_server/src/authentication/authentication_service.ts +++ b/x-pack/packages/security/plugin_types_server/src/authentication/authentication_service.ts @@ -6,8 +6,8 @@ */ import type { KibanaRequest } from '@kbn/core/server'; -import type { AuthenticatedUser } from '@kbn/security-plugin-types-common'; import type { APIKeysService } from '@kbn/core-security-server'; +import type { AuthenticatedUser } from '@kbn/security-plugin-types-common'; /** * Authentication services available on the security plugin's start contract. diff --git a/x-pack/packages/security/plugin_types_server/src/authorization/check_privileges_dynamically.ts b/x-pack/packages/security/plugin_types_server/src/authorization/check_privileges_dynamically.ts index f9663dddc64d0..df250f99bd004 100644 --- a/x-pack/packages/security/plugin_types_server/src/authorization/check_privileges_dynamically.ts +++ b/x-pack/packages/security/plugin_types_server/src/authorization/check_privileges_dynamically.ts @@ -6,9 +6,10 @@ */ import type { KibanaRequest } from '@kbn/core/server'; + import type { - CheckPrivilegesPayload, CheckPrivilegesOptions, + CheckPrivilegesPayload, CheckPrivilegesResponse, } from './check_privileges'; diff --git a/x-pack/packages/security/plugin_types_server/src/authorization/check_saved_objects_privileges.ts b/x-pack/packages/security/plugin_types_server/src/authorization/check_saved_objects_privileges.ts index 4b42723c83286..88d808b105503 100644 --- a/x-pack/packages/security/plugin_types_server/src/authorization/check_saved_objects_privileges.ts +++ b/x-pack/packages/security/plugin_types_server/src/authorization/check_saved_objects_privileges.ts @@ -6,6 +6,7 @@ */ import type { KibanaRequest } from '@kbn/core/server'; + import type { CheckPrivilegesResponse } from './check_privileges'; export type CheckSavedObjectsPrivilegesWithRequest = ( diff --git a/x-pack/packages/security/plugin_types_server/src/authorization/deprecations.ts b/x-pack/packages/security/plugin_types_server/src/authorization/deprecations.ts index 68cc61067e3c0..6567fdebf2b90 100644 --- a/x-pack/packages/security/plugin_types_server/src/authorization/deprecations.ts +++ b/x-pack/packages/security/plugin_types_server/src/authorization/deprecations.ts @@ -6,7 +6,6 @@ */ import type { DeprecationsDetails, GetDeprecationsContext } from '@kbn/core/server'; - import type { Role } from '@kbn/security-plugin-types-common'; export interface PrivilegeDeprecationsRolesByFeatureIdResponse { diff --git a/x-pack/packages/security/plugin_types_server/src/plugin.ts b/x-pack/packages/security/plugin_types_server/src/plugin.ts index c8222163785bf..7d37935ab760a 100644 --- a/x-pack/packages/security/plugin_types_server/src/plugin.ts +++ b/x-pack/packages/security/plugin_types_server/src/plugin.ts @@ -6,9 +6,10 @@ */ import type { SecurityLicense } from '@kbn/security-plugin-types-common'; + import type { AuditServiceSetup } from './audit'; -import type { PrivilegeDeprecationsService, AuthorizationServiceSetup } from './authorization'; import type { AuthenticationServiceStart } from './authentication'; +import type { AuthorizationServiceSetup, PrivilegeDeprecationsService } from './authorization'; import type { UserProfileServiceStart } from './user_profile'; /** diff --git a/x-pack/packages/security/role_management_model/src/__fixtures__/kibana_privileges.ts b/x-pack/packages/security/role_management_model/src/__fixtures__/kibana_privileges.ts index 2dc5078038033..13b34b94bf06b 100644 --- a/x-pack/packages/security/role_management_model/src/__fixtures__/kibana_privileges.ts +++ b/x-pack/packages/security/role_management_model/src/__fixtures__/kibana_privileges.ts @@ -12,8 +12,9 @@ import { subFeaturePrivilegeIterator, } from '@kbn/features-plugin/server/feature_privilege_iterator'; import type { LicenseType } from '@kbn/licensing-plugin/server'; -import type { SecurityLicenseFeatures } from '@kbn/security-plugin-types-common'; import { Actions, privilegesFactory } from '@kbn/security-authorization-core'; +import type { SecurityLicenseFeatures } from '@kbn/security-plugin-types-common'; + import { KibanaPrivileges } from '../kibana_privileges'; const featuresPluginService = (): jest.Mocked => { diff --git a/x-pack/packages/security/role_management_model/src/kibana_privileges.test.ts b/x-pack/packages/security/role_management_model/src/kibana_privileges.test.ts index 6102c853db51b..9fcd04cc2074d 100644 --- a/x-pack/packages/security/role_management_model/src/kibana_privileges.test.ts +++ b/x-pack/packages/security/role_management_model/src/kibana_privileges.test.ts @@ -5,10 +5,11 @@ * 2.0. */ -import { KibanaPrivilege } from './kibana_privilege'; -import { KibanaPrivileges, isGlobalPrivilegeDefinition } from './kibana_privileges'; import type { RoleKibanaPrivilege } from '@kbn/security-plugin-types-common'; + import { createRawKibanaPrivileges, kibanaFeatures } from './__fixtures__'; +import { KibanaPrivilege } from './kibana_privilege'; +import { isGlobalPrivilegeDefinition, KibanaPrivileges } from './kibana_privileges'; describe('kibana_privilege', () => { describe('isGlobalPrivilegeDefinition', () => { diff --git a/x-pack/packages/security/role_management_model/src/kibana_privileges.ts b/x-pack/packages/security/role_management_model/src/kibana_privileges.ts index e78ee9b105bbf..a54ee72cf308a 100644 --- a/x-pack/packages/security/role_management_model/src/kibana_privileges.ts +++ b/x-pack/packages/security/role_management_model/src/kibana_privileges.ts @@ -6,9 +6,9 @@ */ import type { KibanaFeature } from '@kbn/features-plugin/common'; - -import type { RoleKibanaPrivilege } from '@kbn/security-plugin-types-common'; import type { RawKibanaPrivileges } from '@kbn/security-authorization-core'; +import type { RoleKibanaPrivilege } from '@kbn/security-plugin-types-common'; + import { KibanaPrivilege } from './kibana_privilege'; import { PrivilegeCollection } from './privilege_collection'; import { SecuredFeature } from './secured_feature'; diff --git a/x-pack/packages/security/ui_components/src/kibana_privilege_table/feature_table.test.tsx b/x-pack/packages/security/ui_components/src/kibana_privilege_table/feature_table.test.tsx index 2380088dd713f..2c858e7bb6ff6 100644 --- a/x-pack/packages/security/ui_components/src/kibana_privilege_table/feature_table.test.tsx +++ b/x-pack/packages/security/ui_components/src/kibana_privilege_table/feature_table.test.tsx @@ -9,13 +9,13 @@ import { EuiAccordion, EuiIconTip } from '@elastic/eui'; import React from 'react'; import type { KibanaFeature, SubFeatureConfig } from '@kbn/features-plugin/public'; +import type { Role } from '@kbn/security-plugin-types-common'; import { createFeature, createKibanaPrivileges, kibanaFeatures, } from '@kbn/security-role-management-model/src/__fixtures__'; import { findTestSubject, mountWithIntl } from '@kbn/test-jest-helpers'; -import type { Role } from '@kbn/security-plugin-types-common'; import { getDisplayedFeaturePrivileges } from './__fixtures__'; import { FeatureTable } from './feature_table'; diff --git a/x-pack/packages/security/ui_components/src/kibana_privilege_table/feature_table.tsx b/x-pack/packages/security/ui_components/src/kibana_privilege_table/feature_table.tsx index 6fef00ccecec9..2f77b55ce5bac 100644 --- a/x-pack/packages/security/ui_components/src/kibana_privilege_table/feature_table.tsx +++ b/x-pack/packages/security/ui_components/src/kibana_privilege_table/feature_table.tsx @@ -33,9 +33,9 @@ import type { Role } from '@kbn/security-plugin-types-common'; import type { KibanaPrivileges, SecuredFeature } from '@kbn/security-role-management-model'; import { ChangeAllPrivilegesControl } from './change_all_privileges'; +import { FeatureTableCell } from './components/feature_table_cell'; import { FeatureTableExpandedRow } from './feature_table_expanded_row'; import { NO_PRIVILEGE_VALUE } from '../constants'; -import { FeatureTableCell } from './components/feature_table_cell'; import type { PrivilegeFormCalculator } from '../privilege_form_calculator'; interface Props { diff --git a/x-pack/packages/security/ui_components/src/kibana_privilege_table/feature_table_expanded_row.test.tsx b/x-pack/packages/security/ui_components/src/kibana_privilege_table/feature_table_expanded_row.test.tsx index 5e4f4ce021d44..e4f1755604dc5 100644 --- a/x-pack/packages/security/ui_components/src/kibana_privilege_table/feature_table_expanded_row.test.tsx +++ b/x-pack/packages/security/ui_components/src/kibana_privilege_table/feature_table_expanded_row.test.tsx @@ -8,11 +8,11 @@ import { act } from '@testing-library/react'; import React from 'react'; +import type { Role } from '@kbn/security-plugin-types-common'; import { createKibanaPrivileges, kibanaFeatures, } from '@kbn/security-role-management-model/src/__fixtures__'; -import type { Role } from '@kbn/security-plugin-types-common'; import { findTestSubject, mountWithIntl } from '@kbn/test-jest-helpers'; import { FeatureTableExpandedRow } from './feature_table_expanded_row'; diff --git a/x-pack/packages/security/ui_components/src/privilege_form_calculator/privilege_form_calculator.test.ts b/x-pack/packages/security/ui_components/src/privilege_form_calculator/privilege_form_calculator.test.ts index e61134b816ffa..7977ee693721b 100644 --- a/x-pack/packages/security/ui_components/src/privilege_form_calculator/privilege_form_calculator.test.ts +++ b/x-pack/packages/security/ui_components/src/privilege_form_calculator/privilege_form_calculator.test.ts @@ -5,11 +5,11 @@ * 2.0. */ +import type { Role } from '@kbn/security-plugin-types-common'; import { createKibanaPrivileges, kibanaFeatures, } from '@kbn/security-role-management-model/src/__fixtures__'; -import type { Role } from '@kbn/security-plugin-types-common'; import { PrivilegeFormCalculator } from './privilege_form_calculator'; diff --git a/x-pack/plugins/actions/README.md b/x-pack/plugins/actions/README.md index 7cab1ffe0c0b3..4e7f20e47cb7d 100644 --- a/x-pack/plugins/actions/README.md +++ b/x-pack/plugins/actions/README.md @@ -89,13 +89,16 @@ The following table describes the properties of the `options` object. | ------------------------ | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------- | | id | Unique identifier for the action type. For convention, ids starting with `.` are reserved for built in action types. We recommend using a convention like `.mySpecialAction` for your action types. | string | | name | A user-friendly name for the action type. These will be displayed in dropdowns when chosing action types. | string | -| maxAttempts | The maximum number of times this action will attempt to run when scheduled. | number | +| maxAttempts | The maximum number of times this action will attempt to run when scheduled. | number | | minimumLicenseRequired | The license required to use the action type. | string | | supportedFeatureIds | List of IDs of the features that this action type is available in. Allowed values are `alerting`, `siem`, `uptime`, `cases`. See `x-pack/plugins/actions/common/connector_feature_config.ts` for the most up to date list. | string[] | | validate.params | When developing an action type, it needs to accept parameters to know what to do with the action. (Example `to`, `from`, `subject`, `body` of an email). See the current built-in email action type for an example of the state-of-the-art validation.

    Technically, the value of this property should have a property named `validate()` which is a function that takes a params object to validate and returns a sanitized version of that object to pass to the execution function. Validation errors should be thrown from the `validate()` function and will be available as an error message | schema / validation function | | validate.config | Similar to params, a config may be required when creating an action (for example `host` and `port` for an email server). | schema / validation function | | validate.secrets | Similar to params, a secrets object may be required when creating an action (for example `user` and `password` for an email server). | schema / validation function | -| executor | This is where the code of an action type lives. This is a function gets called for generating an action from either alerting or manually by using the exposed function (see firing actions). For full details, see executor section below. | Function | +| executor | This is where the code of an action type lives. This is a function gets called for generating an action from either alerting or manually by using the exposed function (see firing actions). For full details, see executor section below. | Function | +| preSaveHook | This optional function is called before the connector saved object is saved. For full details, see hooks section below. | Function | +| postSaveHook | This optional function is called after the connector saved object is saved. For full details, see hooks section below. | Function | +| postDeleteHook | This optional function is called after the connector saved object is deleted. For full details, see hooks section below. | Function | | renderParameterTemplates | Optionally define a function to provide custom rendering for this action type. | Function | **Important** - The config object is persisted in ElasticSearch and updated via the ElasticSearch update document API. This API allows "partial updates" - and this can cause issues with the encryption used on specified properties. So, a `validate()` function should return values for all configuration properties, so that partial updates do not occur. Setting property values to `null` rather than `undefined`, or not including a property in the config object, is all you need to do to ensure partial updates won't occur. @@ -116,6 +119,71 @@ This is the primary function for an action type. Whenever the action needs to ru | services.savedObjectsClient | This is an instance of the saved objects client. This provides the ability to do CRUD on any saved objects within the same space the alert lives in.

    The scope of the saved objects client is tied to the user in context calling the execute API or the API key provided to the execute plugin function (only when security isenabled). | | services.log(tags, [data], [timestamp]) | Use this to create server logs. (This is the same function as server.log) | +### Hooks + +Hooks allow a connector implementation to be called during connector creation, update, and delete. When not using hooks, the connector implementation is not involved in creation, update and delete, except for the schema validation that happens for creation and update. Hooks can be used to force a create or update to fail, or run arbitrary code before and after update and create, and after delete. We don't have a need for a hook before delete at the moment, so that hook is currently not available. + +Hooks are passed the following parameters: + +```ts +interface PreSaveConnectorHookParams { + connectorId: string; + config: Config; + secrets: Secrets; + logger: Logger; + request: KibanaRequest; + services: HookServices; + isUpdate: boolean; +} + +interface PostSaveConnectorHookParams { + connectorId: string; + config: Config; + secrets: Secrets; + logger: Logger; + request: KibanaRequest; + services: HookServices; + isUpdate: boolean; + wasSuccessful: boolean; +} + +interface PostDeleteConnectorHookParams { + connectorId: string; + config: Config; + // secrets not provided, yet + logger: Logger; + request: KibanaRequest; + services: HookServices; +} +``` + +| parameter | description +| --------- | ----------- +| `connectorId` | The id of the connector. +| `config` | The connector's `config` object. +| `secrets` | The connector's `secrets` object. +| `logger` | A standard Kibana logger. +| `request` | The request causing this operation +| `services` | Common service objects, see below. +| `isUpdate` | For the `PreSave` and `PostSave` hooks, `isUpdate` is false for create operations, and true for update operations. +| `wasSuccessful` | For the `PostSave` hook, this indicates if the connector was persisted as a Saved Object successfully. + +The `services` object contains the following properties: + +| property | description +| --------- | ----------- +| `scopedClusterClient` | A standard `scopeClusterClient` object. + +The hooks are called just before, and just after, the Saved Object operation for the client methods is invoked. + +The `PostDelete` hook does not have a `wasSuccessful` property, as the hook is not called if the delete operation fails. The saved object will still exist. Only a successful call to delete the connector will cause the hook to run. + +The `PostSave` hook is useful if the `PreSave` hook is creating / modifying other resources. The `PreSave` hook is called just before the connector SO is actually created/updated, and of course that create/update could fail for some reason. In those cases, the `PostSave` hook is passed `wasSuccessful: false` and can "undo" any work it did in the `PreSave` hook. + +The `PreSave` hook can be used to cancel a create or update, by throwing an exception. The `PostSave` and `PostDelete` invocations will have thrown exceptions caught and logged to the Kibana log, and will not cancel the operation. + +When throwing an error in the `PreSave` hook, the Error's message will be used as the error failing the operation, so should include a human-readable description of what it was doing, along with any message from an underlying API that failed, if available. When an error is thrown from a `PreSave` hook, the `PostSave` hook will **NOT** be run. + ### Example The built-in email action type provides a good example of creating an action type with non-trivial configuration and params: diff --git a/x-pack/plugins/actions/server/actions_client/actions_client.test.ts b/x-pack/plugins/actions/server/actions_client/actions_client.test.ts index 46e73f7bb3591..7f15dd6287d6b 100644 --- a/x-pack/plugins/actions/server/actions_client/actions_client.test.ts +++ b/x-pack/plugins/actions/server/actions_client/actions_client.test.ts @@ -113,6 +113,9 @@ const mockTaskManager = taskManagerMock.createSetup(); const configurationUtilities = actionsConfigMock.create(); const eventLogClient = eventLogClientMock.create(); const getEventLogClient = jest.fn(); +const preSaveHook = jest.fn(); +const postSaveHook = jest.fn(); +const postDeleteHook = jest.fn(); let actionsClient: ActionsClient; let mockedLicenseState: jest.Mocked; @@ -392,6 +395,8 @@ describe('create()', () => { params: { schema: schema.object({}) }, }, executor, + preSaveHook, + postSaveHook, }); unsecuredSavedObjectsClient.create.mockResolvedValueOnce(savedObjectCreateResult); const result = await actionsClient.create({ @@ -428,6 +433,8 @@ describe('create()', () => { }, ] `); + expect(preSaveHook).toHaveBeenCalledTimes(1); + expect(postSaveHook).toHaveBeenCalledTimes(1); }); test('validates config', async () => { @@ -1973,6 +1980,33 @@ describe('getOAuthAccessToken()', () => { }); describe('delete()', () => { + beforeEach(() => { + actionTypeRegistry.register({ + id: 'my-action-delete', + name: 'My action type', + minimumLicenseRequired: 'basic', + supportedFeatureIds: ['alerting'], + validate: { + config: { schema: schema.object({}) }, + secrets: { schema: schema.object({}) }, + params: { schema: schema.object({}) }, + }, + executor, + postDeleteHook: async (options) => postDeleteHook(options), + }); + unsecuredSavedObjectsClient.get.mockResolvedValueOnce({ + id: '1', + type: 'action', + attributes: { + actionTypeId: 'my-action-delete', + isMissingSecrets: false, + config: {}, + secrets: {}, + }, + references: [], + }); + }); + describe('authorization', () => { test('ensures user is authorised to delete actions', async () => { await actionsClient.delete({ id: '1' }); @@ -2052,6 +2086,16 @@ describe('delete()', () => { `); }); + test('calls postDeleteHook', async () => { + const expectedResult = Symbol(); + unsecuredSavedObjectsClient.delete.mockResolvedValueOnce(expectedResult); + + const result = await actionsClient.delete({ id: '1' }); + expect(result).toEqual(expectedResult); + expect(unsecuredSavedObjectsClient.delete).toHaveBeenCalledTimes(1); + expect(postDeleteHook).toHaveBeenCalledTimes(1); + }); + it('throws when trying to delete a preconfigured connector', async () => { actionsClient = new ActionsClient({ logger, @@ -2250,6 +2294,8 @@ describe('update()', () => { params: { schema: schema.object({}) }, }, executor, + preSaveHook, + postSaveHook, }); unsecuredSavedObjectsClient.get.mockResolvedValueOnce({ id: '1', @@ -2315,6 +2361,9 @@ describe('update()', () => { "my-action", ] `); + + expect(preSaveHook).toHaveBeenCalledTimes(1); + expect(postSaveHook).toHaveBeenCalledTimes(1); }); test('updates an action with isMissingSecrets "true" (set true as the import result), to isMissingSecrets', async () => { diff --git a/x-pack/plugins/actions/server/actions_client/actions_client.ts b/x-pack/plugins/actions/server/actions_client/actions_client.ts index 7e4d72faedaed..f485d82b2f120 100644 --- a/x-pack/plugins/actions/server/actions_client/actions_client.ts +++ b/x-pack/plugins/actions/server/actions_client/actions_client.ts @@ -43,6 +43,7 @@ import { validateConnector, ActionExecutionSource, parseDate, + tryCatch, } from '../lib'; import { ActionResult, @@ -50,6 +51,7 @@ import { InMemoryConnector, ActionTypeExecutorResult, ConnectorTokenClientContract, + HookServices, } from '../types'; import { PreconfiguredActionDisabledModificationError } from '../lib/errors/preconfigured_action_disabled_modification'; import { ExecuteOptions } from '../lib/action_executor'; @@ -246,6 +248,33 @@ export class ActionsClient { } this.context.actionTypeRegistry.ensureActionTypeEnabled(actionTypeId); + const hookServices: HookServices = { + scopedClusterClient: this.context.scopedClusterClient, + }; + + if (actionType.preSaveHook) { + try { + await actionType.preSaveHook({ + connectorId: id, + config, + secrets, + logger: this.context.logger, + request: this.context.request, + services: hookServices, + isUpdate: false, + }); + } catch (error) { + this.context.auditLogger?.log( + connectorAuditEvent({ + action: ConnectorAuditAction.CREATE, + savedObject: { type: 'action', id }, + error, + }) + ); + throw error; + } + } + this.context.auditLogger?.log( connectorAuditEvent({ action: ConnectorAuditAction.CREATE, @@ -254,18 +283,48 @@ export class ActionsClient { }) ); - const result = await this.context.unsecuredSavedObjectsClient.create( - 'action', - { - actionTypeId, - name, - isMissingSecrets: false, - config: validatedActionTypeConfig as SavedObjectAttributes, - secrets: validatedActionTypeSecrets as SavedObjectAttributes, - }, - { id } + const result = await tryCatch( + async () => + await this.context.unsecuredSavedObjectsClient.create( + 'action', + { + actionTypeId, + name, + isMissingSecrets: false, + config: validatedActionTypeConfig as SavedObjectAttributes, + secrets: validatedActionTypeSecrets as SavedObjectAttributes, + }, + { id } + ) ); + const wasSuccessful = !(result instanceof Error); + const label = `connectorId: "${id}"; type: ${actionTypeId}`; + const tags = ['post-save-hook', id]; + + if (actionType.postSaveHook) { + try { + await actionType.postSaveHook({ + connectorId: id, + config, + secrets, + logger: this.context.logger, + request: this.context.request, + services: hookServices, + isUpdate: false, + wasSuccessful, + }); + } catch (err) { + this.context.logger.error(`postSaveHook create error for ${label}: ${err.message}`, { + tags, + }); + } + } + + if (!wasSuccessful) { + throw result; + } + return { id: result.id, actionTypeId: result.attributes.actionTypeId, @@ -558,7 +617,36 @@ export class ActionsClient { ); } - return await this.context.unsecuredSavedObjectsClient.delete('action', id); + const rawAction = await this.context.unsecuredSavedObjectsClient.get('action', id); + const { + attributes: { actionTypeId, config }, + } = rawAction; + + const actionType = this.context.actionTypeRegistry.get(actionTypeId); + const result = await this.context.unsecuredSavedObjectsClient.delete('action', id); + + const hookServices: HookServices = { + scopedClusterClient: this.context.scopedClusterClient, + }; + + if (actionType.postDeleteHook) { + try { + await actionType.postDeleteHook({ + connectorId: id, + config, + logger: this.context.logger, + request: this.context.request, + services: hookServices, + }); + } catch (error) { + const tags = ['post-delete-hook', id]; + this.context.logger.error( + `The post delete hook failed for for connector "${id}": ${error.message}`, + { tags } + ); + } + } + return result; } private getSystemActionKibanaPrivileges(connectorId: string, params?: ExecuteOptions['params']) { diff --git a/x-pack/plugins/actions/server/actions_client/actions_client_hooks.test.ts b/x-pack/plugins/actions/server/actions_client/actions_client_hooks.test.ts new file mode 100644 index 0000000000000..7a1a0fb5e3d91 --- /dev/null +++ b/x-pack/plugins/actions/server/actions_client/actions_client_hooks.test.ts @@ -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; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { omit } from 'lodash'; +import { schema } from '@kbn/config-schema'; +import { MockedLogger, loggerMock } from '@kbn/logging-mocks'; +import { ActionTypeRegistry, ActionTypeRegistryOpts } from '../action_type_registry'; +import { ActionsClient } from './actions_client'; +import { ExecutorType } from '../types'; +import { ActionExecutor, TaskRunnerFactory, ILicenseState } from '../lib'; +import { taskManagerMock } from '@kbn/task-manager-plugin/server/mocks'; +import { actionsConfigMock } from '../actions_config.mock'; +import { licenseStateMock } from '../lib/license_state.mock'; +import { licensingMock } from '@kbn/licensing-plugin/server/mocks'; +import { + httpServerMock, + 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 { actionExecutorMock } from '../lib/action_executor.mock'; +import { ActionsAuthorization } from '../authorization/actions_authorization'; +import { actionsAuthorizationMock } from '../authorization/actions_authorization.mock'; +import { connectorTokenClientMock } from '../lib/connector_token_client.mock'; +import { inMemoryMetricsMock } from '../monitoring/in_memory_metrics.mock'; + +jest.mock('uuid', () => ({ + v4: () => ConnectorSavedObject.id, +})); + +const kibanaIndices = ['.kibana']; +const unsecuredSavedObjectsClient = savedObjectsClientMock.create(); +const scopedClusterClient = elasticsearchServiceMock.createScopedClusterClient(); +const actionExecutor = actionExecutorMock.create(); +const authorization = actionsAuthorizationMock.create(); +const ephemeralExecutionEnqueuer = jest.fn(); +const bulkExecutionEnqueuer = jest.fn(); +const request = httpServerMock.createKibanaRequest(); +const auditLogger = auditLoggerMock.create(); +const mockUsageCountersSetup = usageCountersServiceMock.createSetupContract(); +const mockUsageCounter = mockUsageCountersSetup.createUsageCounter('test'); +const mockTaskManager = taskManagerMock.createSetup(); +const getEventLogClient = jest.fn(); +const preSaveHook = jest.fn(); +const postSaveHook = jest.fn(); +const postDeleteHook = jest.fn(); + +let actionsClient: ActionsClient; +let mockedLicenseState: jest.Mocked; +let actionTypeRegistry: ActionTypeRegistry; +let actionTypeRegistryParams: ActionTypeRegistryOpts; +const executor: ExecutorType<{}, {}, {}, void> = async (options) => { + return { status: 'ok', actionId: options.actionId }; +}; + +const ConnectorSavedObject = { + id: 'connector-id-uuid', + type: 'action', + attributes: { + actionTypeId: 'hooked-action-type', + isMissingSecrets: false, + name: 'Hooked Action', + config: { foo: 42 }, + secrets: { bar: 2001 }, + }, + references: [], +}; + +const CreateParms = { + action: { + name: ConnectorSavedObject.attributes.name, + actionTypeId: ConnectorSavedObject.attributes.actionTypeId, + config: ConnectorSavedObject.attributes.config, + secrets: ConnectorSavedObject.attributes.secrets, + }, +}; + +const UpdateParms = { + id: ConnectorSavedObject.id, + action: { + name: ConnectorSavedObject.attributes.name, + config: ConnectorSavedObject.attributes.config, + secrets: ConnectorSavedObject.attributes.secrets, + }, +}; + +const CoreHookParams = { + connectorId: ConnectorSavedObject.id, + config: ConnectorSavedObject.attributes.config, + secrets: ConnectorSavedObject.attributes.secrets, + request, + services: { + // this will be checked with a function test + scopedClusterClient: expect.any(Object), + }, +}; + +const connectorTokenClient = connectorTokenClientMock.create(); +const inMemoryMetrics = inMemoryMetricsMock.create(); + +let logger: MockedLogger; + +beforeEach(() => { + jest.resetAllMocks(); + logger = loggerMock.create(); + mockedLicenseState = licenseStateMock.create(); + + actionTypeRegistryParams = { + licensing: licensingMock.createSetup(), + taskManager: mockTaskManager, + taskRunnerFactory: new TaskRunnerFactory( + new ActionExecutor({ isESOCanEncrypt: true }), + inMemoryMetrics + ), + actionsConfigUtils: actionsConfigMock.create(), + licenseState: mockedLicenseState, + inMemoryConnectors: [], + }; + + actionTypeRegistry = new ActionTypeRegistry(actionTypeRegistryParams); + actionsClient = new ActionsClient({ + logger, + actionTypeRegistry, + unsecuredSavedObjectsClient, + scopedClusterClient, + kibanaIndices, + inMemoryConnectors: [], + actionExecutor, + ephemeralExecutionEnqueuer, + bulkExecutionEnqueuer, + request, + authorization: authorization as unknown as ActionsAuthorization, + auditLogger, + usageCounter: mockUsageCounter, + connectorTokenClient, + getEventLogClient, + }); + + actionTypeRegistry.register({ + id: 'hooked-action-type', + name: 'Hooked action type', + minimumLicenseRequired: 'gold', + supportedFeatureIds: ['alerting'], + validate: { + config: { schema: schema.object({ foo: schema.number() }) }, + secrets: { schema: schema.object({ bar: schema.number() }) }, + params: { schema: schema.object({}) }, + }, + executor, + preSaveHook, + postSaveHook, + postDeleteHook, + }); +}); + +describe('connector type hooks', () => { + describe('successful operation and successful hook', () => { + test('for create', async () => { + unsecuredSavedObjectsClient.create.mockResolvedValueOnce(ConnectorSavedObject); + const result = await actionsClient.create(CreateParms); + expect(result.id).toBe(ConnectorSavedObject.id); + + const preParams = { ...CoreHookParams, logger, isUpdate: false }; + const postParams = { ...preParams, wasSuccessful: true }; + + expect(preSaveHook).toHaveBeenCalledTimes(1); + expect(preSaveHook.mock.calls[0]).toStrictEqual([preParams]); + + expect(postSaveHook).toHaveBeenCalledTimes(1); + expect(postSaveHook.mock.calls[0]).toStrictEqual([postParams]); + }); + + test('for update', async () => { + unsecuredSavedObjectsClient.get.mockResolvedValueOnce(ConnectorSavedObject); + unsecuredSavedObjectsClient.create.mockResolvedValueOnce(ConnectorSavedObject); + const result = await actionsClient.update(UpdateParms); + expect(result.id).toBe(ConnectorSavedObject.id); + + const preParams = { ...CoreHookParams, logger, isUpdate: true }; + const postParams = { ...preParams, wasSuccessful: true }; + + expect(preSaveHook).toHaveBeenCalledTimes(1); + expect(preSaveHook.mock.calls[0]).toStrictEqual([preParams]); + + expect(postSaveHook).toHaveBeenCalledTimes(1); + expect(postSaveHook.mock.calls[0]).toStrictEqual([postParams]); + }); + + test('for delete', async () => { + const expectedResult = Symbol(); + unsecuredSavedObjectsClient.delete.mockResolvedValueOnce(expectedResult); + unsecuredSavedObjectsClient.get.mockResolvedValueOnce(ConnectorSavedObject); + + const result = await actionsClient.delete({ id: ConnectorSavedObject.id }); + expect(result).toBe(expectedResult); + + const postParamsWithSecrets = { ...CoreHookParams, logger }; + const postParams = omit(postParamsWithSecrets, 'secrets'); + + expect(postDeleteHook).toHaveBeenCalledTimes(1); + expect(postDeleteHook.mock.calls[0]).toEqual([postParams]); + }); + }); + + describe('unsuccessful operation and successful hook', () => { + test('for create', async () => { + unsecuredSavedObjectsClient.create.mockRejectedValueOnce(new Error('OMG create')); + await expect(actionsClient.create(CreateParms)).rejects.toMatchInlineSnapshot( + `[Error: OMG create]` + ); + + const preParams = { ...CoreHookParams, logger, isUpdate: false }; + const postParams = { ...preParams, wasSuccessful: false }; + + expect(preSaveHook).toHaveBeenCalledTimes(1); + expect(preSaveHook.mock.calls[0]).toStrictEqual([preParams]); + + expect(postSaveHook).toHaveBeenCalledTimes(1); + expect(postSaveHook.mock.calls[0]).toStrictEqual([postParams]); + }); + + test('for update', async () => { + unsecuredSavedObjectsClient.get.mockResolvedValueOnce(ConnectorSavedObject); + unsecuredSavedObjectsClient.create.mockRejectedValueOnce(new Error('OMG update')); + await expect(actionsClient.update(UpdateParms)).rejects.toMatchInlineSnapshot( + `[Error: OMG update]` + ); + + const preParams = { ...CoreHookParams, logger, isUpdate: true }; + const postParams = { ...preParams, wasSuccessful: false }; + + expect(preSaveHook).toHaveBeenCalledTimes(1); + expect(preSaveHook.mock.calls[0]).toStrictEqual([preParams]); + + expect(postSaveHook).toHaveBeenCalledTimes(1); + expect(postSaveHook.mock.calls[0]).toStrictEqual([postParams]); + }); + + test('for delete', async () => { + unsecuredSavedObjectsClient.get.mockResolvedValueOnce(ConnectorSavedObject); + unsecuredSavedObjectsClient.delete.mockRejectedValueOnce(new Error('OMG delete')); + + await expect( + actionsClient.delete({ id: ConnectorSavedObject.id }) + ).rejects.toMatchInlineSnapshot(`[Error: OMG delete]`); + + expect(postDeleteHook).toHaveBeenCalledTimes(0); + }); + }); + + describe('successful operation and unsuccessful hook', () => { + test('for create pre hook', async () => { + preSaveHook.mockRejectedValueOnce(new Error('OMG create pre save')); + + await expect(actionsClient.create(CreateParms)).rejects.toMatchInlineSnapshot( + `[Error: OMG create pre save]` + ); + + const preParams = { ...CoreHookParams, logger, isUpdate: false }; + + expect(preSaveHook).toHaveBeenCalledTimes(1); + expect(preSaveHook.mock.calls[0]).toStrictEqual([preParams]); + + expect(unsecuredSavedObjectsClient.create).toHaveBeenCalledTimes(0); + expect(postSaveHook).toHaveBeenCalledTimes(0); + }); + + test('for create post hook', async () => { + postSaveHook.mockRejectedValueOnce(new Error('OMG create post save')); + + unsecuredSavedObjectsClient.create.mockResolvedValueOnce(ConnectorSavedObject); + const result = await actionsClient.create(CreateParms); + expect(result.id).toBe(ConnectorSavedObject.id); + + const preParams = { ...CoreHookParams, logger, isUpdate: false }; + const postParams = { ...preParams, wasSuccessful: true }; + + expect(preSaveHook).toHaveBeenCalledTimes(1); + expect(preSaveHook.mock.calls[0]).toStrictEqual([preParams]); + + expect(postSaveHook).toHaveBeenCalledTimes(1); + expect(postSaveHook.mock.calls[0]).toStrictEqual([postParams]); + expect(logger.error).toHaveBeenCalledTimes(1); + expect(logger.error.mock.calls).toMatchInlineSnapshot(` + Array [ + Array [ + "postSaveHook create error for connectorId: \\"connector-id-uuid\\"; type: hooked-action-type: OMG create post save", + Object { + "tags": Array [ + "post-save-hook", + "connector-id-uuid", + ], + }, + ], + ] + `); + }); + + test('for update pre hook', async () => { + preSaveHook.mockRejectedValueOnce(new Error('OMG update pre save')); + + unsecuredSavedObjectsClient.get.mockResolvedValueOnce(ConnectorSavedObject); + unsecuredSavedObjectsClient.create.mockResolvedValueOnce(ConnectorSavedObject); + await expect(actionsClient.update(UpdateParms)).rejects.toMatchInlineSnapshot( + `[Error: OMG update pre save]` + ); + + const preParams = { ...CoreHookParams, logger, isUpdate: true }; + + expect(preSaveHook).toHaveBeenCalledTimes(1); + expect(preSaveHook.mock.calls[0]).toStrictEqual([preParams]); + + expect(unsecuredSavedObjectsClient.create).toHaveBeenCalledTimes(0); + expect(postSaveHook).toHaveBeenCalledTimes(0); + }); + + test('for update post hook', async () => { + postSaveHook.mockRejectedValueOnce(new Error('OMG update post save')); + + unsecuredSavedObjectsClient.get.mockResolvedValueOnce(ConnectorSavedObject); + unsecuredSavedObjectsClient.create.mockResolvedValueOnce(ConnectorSavedObject); + const result = await actionsClient.update(UpdateParms); + expect(result.id).toBe(ConnectorSavedObject.id); + + const preParams = { ...CoreHookParams, logger, isUpdate: true }; + const postParams = { ...preParams, wasSuccessful: true }; + + expect(preSaveHook).toHaveBeenCalledTimes(1); + expect(preSaveHook.mock.calls[0]).toStrictEqual([preParams]); + + expect(postSaveHook).toHaveBeenCalledTimes(1); + expect(postSaveHook.mock.calls[0]).toStrictEqual([postParams]); + expect(logger.error).toHaveBeenCalledTimes(1); + expect(logger.error.mock.calls).toMatchInlineSnapshot(` + Array [ + Array [ + "postSaveHook update error for connectorId: \\"connector-id-uuid\\"; type: hooked-action-type: OMG update post save", + Object { + "tags": Array [ + "post-save-hook", + "connector-id-uuid", + ], + }, + ], + ] + `); + }); + + test('for delete post hook', async () => { + postDeleteHook.mockRejectedValueOnce(new Error('OMG delete post delete')); + + const expectedResult = Symbol(); + unsecuredSavedObjectsClient.delete.mockResolvedValueOnce(expectedResult); + unsecuredSavedObjectsClient.get.mockResolvedValueOnce(ConnectorSavedObject); + + const result = await actionsClient.delete({ id: ConnectorSavedObject.id }); + expect(result).toBe(expectedResult); + + const postParamsWithSecrets = { ...CoreHookParams, logger }; + const postParams = omit(postParamsWithSecrets, 'secrets'); + + expect(postDeleteHook).toHaveBeenCalledTimes(1); + expect(postDeleteHook.mock.calls[0]).toEqual([postParams]); + expect(logger.error).toHaveBeenCalledTimes(1); + expect(logger.error.mock.calls).toMatchInlineSnapshot(` + Array [ + Array [ + "The post delete hook failed for for connector \\"connector-id-uuid\\": OMG delete post delete", + Object { + "tags": Array [ + "post-delete-hook", + "connector-id-uuid", + ], + }, + ], + ] + `); + }); + }); +}); diff --git a/x-pack/plugins/actions/server/application/connector/methods/update/update.ts b/x-pack/plugins/actions/server/application/connector/methods/update/update.ts index 7baa099a29029..e22715c31d149 100644 --- a/x-pack/plugins/actions/server/application/connector/methods/update/update.ts +++ b/x-pack/plugins/actions/server/application/connector/methods/update/update.ts @@ -15,7 +15,8 @@ import { PreconfiguredActionDisabledModificationError } from '../../../../lib/er import { ConnectorAuditAction, connectorAuditEvent } from '../../../../lib/audit_events'; import { validateConfig, validateConnector, validateSecrets } from '../../../../lib'; import { isConnectorDeprecated } from '../../lib'; -import { RawAction } from '../../../../types'; +import { RawAction, HookServices } from '../../../../types'; +import { tryCatch } from '../../../../lib'; export async function update({ context, id, action }: ConnectorUpdateParams): Promise { try { @@ -75,6 +76,33 @@ export async function update({ context, id, action }: ConnectorUpdateParams): Pr context.actionTypeRegistry.ensureActionTypeEnabled(actionTypeId); + const hookServices: HookServices = { + scopedClusterClient: context.scopedClusterClient, + }; + + if (actionType.preSaveHook) { + try { + await actionType.preSaveHook({ + connectorId: id, + config, + secrets, + logger: context.logger, + request: context.request, + services: hookServices, + isUpdate: true, + }); + } catch (error) { + context.auditLogger?.log( + connectorAuditEvent({ + action: ConnectorAuditAction.UPDATE, + savedObject: { type: 'action', id }, + error, + }) + ); + throw error; + } + } + context.auditLogger?.log( connectorAuditEvent({ action: ConnectorAuditAction.UPDATE, @@ -83,27 +111,57 @@ export async function update({ context, id, action }: ConnectorUpdateParams): Pr }) ); - const result = await context.unsecuredSavedObjectsClient.create( - 'action', - { - ...attributes, - actionTypeId, - name, - isMissingSecrets: false, - config: validatedActionTypeConfig as SavedObjectAttributes, - secrets: validatedActionTypeSecrets as SavedObjectAttributes, - }, - omitBy( - { - id, - overwrite: true, - references, - version, - }, - isUndefined - ) + const result = await tryCatch( + async () => + await context.unsecuredSavedObjectsClient.create( + 'action', + { + ...attributes, + actionTypeId, + name, + isMissingSecrets: false, + config: validatedActionTypeConfig as SavedObjectAttributes, + secrets: validatedActionTypeSecrets as SavedObjectAttributes, + }, + omitBy( + { + id, + overwrite: true, + references, + version, + }, + isUndefined + ) + ) ); + const wasSuccessful = !(result instanceof Error); + const label = `connectorId: "${id}"; type: ${actionTypeId}`; + const tags = ['post-save-hook', id]; + + if (actionType.postSaveHook) { + try { + await actionType.postSaveHook({ + connectorId: id, + config, + secrets, + logger: context.logger, + request: context.request, + services: hookServices, + isUpdate: true, + wasSuccessful, + }); + } catch (err) { + context.logger.error(`postSaveHook update error for ${label}: ${err.message}`, { + tags, + }); + } + } + + if (!wasSuccessful) { + throw result; + } + try { await context.connectorTokenClient.deleteConnectorTokens({ connectorId: id }); } catch (e) { diff --git a/x-pack/plugins/actions/server/create_execute_function.test.ts b/x-pack/plugins/actions/server/create_execute_function.test.ts index a1ab85933d9bc..7be187743e634 100644 --- a/x-pack/plugins/actions/server/create_execute_function.test.ts +++ b/x-pack/plugins/actions/server/create_execute_function.test.ts @@ -1088,6 +1088,7 @@ describe('bulkExecute()', () => { "actionTypeId": "mock-action", "id": "123", "response": "queuedActionsLimitError", + "uuid": undefined, }, ], } @@ -1099,4 +1100,93 @@ describe('bulkExecute()', () => { ] `); }); + + test('passes through action uuid if provided', async () => { + mockTaskManager.aggregate.mockResolvedValue({ + took: 1, + timed_out: false, + _shards: { total: 1, successful: 1, skipped: 0, failed: 0 }, + hits: { total: { value: 2, relation: 'eq' }, max_score: null, hits: [] }, + aggregations: {}, + }); + mockActionsConfig.getMaxQueued.mockReturnValueOnce(3); + const executeFn = createBulkExecutionEnqueuerFunction({ + taskManager: mockTaskManager, + actionTypeRegistry: actionTypeRegistryMock.create(), + isESOCanEncrypt: true, + inMemoryConnectors: [], + configurationUtilities: mockActionsConfig, + logger: mockLogger, + }); + savedObjectsClient.bulkGet.mockResolvedValueOnce({ + saved_objects: [ + { id: '123', type: 'action', attributes: { actionTypeId: 'mock-action' }, references: [] }, + ], + }); + savedObjectsClient.bulkCreate.mockResolvedValueOnce({ + saved_objects: [ + { id: '234', type: 'action_task_params', attributes: { actionId: '123' }, references: [] }, + ], + }); + expect( + await executeFn(savedObjectsClient, [ + { + id: '123', + params: { baz: false }, + spaceId: 'default', + executionId: '123abc', + apiKey: null, + source: asHttpRequestExecutionSource(request), + actionTypeId: 'mock-action', + uuid: 'aaa', + }, + { + id: '123', + params: { baz: false }, + spaceId: 'default', + executionId: '456xyz', + apiKey: null, + source: asHttpRequestExecutionSource(request), + actionTypeId: 'mock-action', + uuid: 'bbb', + }, + ]) + ).toMatchInlineSnapshot(` + Object { + "errors": true, + "items": Array [ + Object { + "actionTypeId": "mock-action", + "id": "123", + "response": "success", + "uuid": "aaa", + }, + Object { + "actionTypeId": "mock-action", + "id": "123", + "response": "queuedActionsLimitError", + "uuid": "bbb", + }, + ], + } + `); + expect(mockTaskManager.bulkSchedule).toHaveBeenCalledTimes(1); + expect(mockTaskManager.bulkSchedule.mock.calls[0]).toMatchInlineSnapshot(` + Array [ + Array [ + Object { + "params": Object { + "actionTaskParamsId": "234", + "spaceId": "default", + }, + "scope": Array [ + "actions", + ], + "state": Object {}, + "taskType": "actions:mock-action", + }, + ], + ] + `); + }); }); diff --git a/x-pack/plugins/actions/server/create_execute_function.ts b/x-pack/plugins/actions/server/create_execute_function.ts index e8f9c859747ff..a92bff9719559 100644 --- a/x-pack/plugins/actions/server/create_execute_function.ts +++ b/x-pack/plugins/actions/server/create_execute_function.ts @@ -31,6 +31,7 @@ interface CreateExecuteFunctionOptions { export interface ExecuteOptions extends Pick { id: string; + uuid?: string; spaceId: string; apiKey: string | null; executionId: string; @@ -71,6 +72,7 @@ export interface ExecutionResponse { export interface ExecutionResponseItem { id: string; + uuid?: string; actionTypeId: string; response: ExecutionResponseType; } @@ -197,12 +199,14 @@ export function createBulkExecutionEnqueuerFunction({ items: runnableActions .map((a) => ({ id: a.id, + uuid: a.uuid, actionTypeId: a.actionTypeId, response: ExecutionResponseType.SUCCESS, })) .concat( actionsOverLimit.map((a) => ({ id: a.id, + uuid: a.uuid, actionTypeId: a.actionTypeId, response: ExecutionResponseType.QUEUED_ACTIONS_LIMIT_ERROR, })) diff --git a/x-pack/plugins/actions/server/lib/index.ts b/x-pack/plugins/actions/server/lib/index.ts index 9b8d452f446a9..e13fb85008a84 100644 --- a/x-pack/plugins/actions/server/lib/index.ts +++ b/x-pack/plugins/actions/server/lib/index.ts @@ -38,3 +38,4 @@ export { export { parseDate } from './parse_date'; export type { RelatedSavedObjects } from './related_saved_objects'; export { getBasicAuthHeader, combineHeadersWithBasicAuthHeader } from './get_basic_auth_header'; +export { tryCatch } from './try_catch'; diff --git a/x-pack/plugins/actions/server/lib/try_catch.ts b/x-pack/plugins/actions/server/lib/try_catch.ts new file mode 100644 index 0000000000000..a9932601c8256 --- /dev/null +++ b/x-pack/plugins/actions/server/lib/try_catch.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. + */ + +// functional version of try/catch, allows you to not have to use +// `let` vars initialied to `undefined` to capture the result value + +export async function tryCatch(fn: () => Promise): Promise { + try { + return await fn(); + } catch (err) { + return err; + } +} diff --git a/x-pack/plugins/actions/server/sub_action_framework/register.test.ts b/x-pack/plugins/actions/server/sub_action_framework/register.test.ts index a0e56c1a39b80..8ae7f3cf3350f 100644 --- a/x-pack/plugins/actions/server/sub_action_framework/register.test.ts +++ b/x-pack/plugins/actions/server/sub_action_framework/register.test.ts @@ -21,6 +21,9 @@ import { ServiceParams } from './types'; describe('Registration', () => { const renderedVariables = { body: '' }; const mockRenderParameterTemplates = jest.fn().mockReturnValue(renderedVariables); + const mockPreSaveHook = jest.fn(); + const mockPostSaveHook = jest.fn(); + const mockPostDeleteHook = jest.fn(); const connector = { id: '.test', @@ -47,7 +50,12 @@ describe('Registration', () => { it('registers the connector correctly', async () => { register({ actionTypeRegistry, - connector, + connector: { + ...connector, + preSaveHook: mockPreSaveHook, + postSaveHook: mockPostSaveHook, + postDeleteHook: mockPostDeleteHook, + }, configurationUtilities: mockedActionsConfig, logger, }); @@ -62,6 +70,9 @@ describe('Registration', () => { executor: expect.any(Function), getService: expect.any(Function), renderParameterTemplates: expect.any(Function), + preSaveHook: expect.any(Function), + postSaveHook: expect.any(Function), + postDeleteHook: expect.any(Function), }); }); diff --git a/x-pack/plugins/actions/server/sub_action_framework/register.ts b/x-pack/plugins/actions/server/sub_action_framework/register.ts index dd05cc4e99967..04e7f0d9ea417 100644 --- a/x-pack/plugins/actions/server/sub_action_framework/register.ts +++ b/x-pack/plugins/actions/server/sub_action_framework/register.ts @@ -43,5 +43,8 @@ export const register = { /** @@ -76,6 +77,35 @@ export type Validators = Array< ConfigValidator | SecretsValidator >; +export interface PreSaveConnectorHookParams { + connectorId: string; + config: Config; + secrets: Secrets; + logger: Logger; + request: KibanaRequest; + services: HookServices; + isUpdate: boolean; +} + +export interface PostSaveConnectorHookParams { + connectorId: string; + config: Config; + secrets: Secrets; + logger: Logger; + request: KibanaRequest; + services: HookServices; + isUpdate: boolean; + wasSuccessful: boolean; +} + +export interface PostDeleteConnectorHookParams { + connectorId: string; + config: Config; + logger: Logger; + services: HookServices; + request: KibanaRequest; +} + export interface SubActionConnectorType { id: string; name: string; @@ -92,6 +122,9 @@ export interface SubActionConnectorType { getKibanaPrivileges?: (args?: { params?: { subAction: string; subActionParams: Record }; }) => string[]; + preSaveHook?: (params: PreSaveConnectorHookParams) => Promise; + postSaveHook?: (params: PostSaveConnectorHookParams) => Promise; + postDeleteHook?: (params: PostDeleteConnectorHookParams) => Promise; } export interface ExecutorParams extends ActionTypeParams { diff --git a/x-pack/plugins/actions/server/types.ts b/x-pack/plugins/actions/server/types.ts index 487e7630d40f9..d7c3497edc376 100644 --- a/x-pack/plugins/actions/server/types.ts +++ b/x-pack/plugins/actions/server/types.ts @@ -16,6 +16,7 @@ import { SavedObjectReference, Logger, ISavedObjectsRepository, + IScopedClusterClient, } from '@kbn/core/server'; import { AnySchema } from 'joi'; import { SubActionConnector } from './sub_action_framework/sub_action_connector'; @@ -57,6 +58,10 @@ export interface UnsecuredServices { connectorTokenClient: ConnectorTokenClient; } +export interface HookServices { + scopedClusterClient: IScopedClusterClient; +} + export interface ActionsApiRequestHandlerContext { getActionsClient: () => ActionsClient; listTypes: ActionTypeRegistry['list']; @@ -138,6 +143,44 @@ export type RenderParameterTemplates = ( actionId?: string ) => Params; +export interface PreSaveConnectorHookParams< + Config extends ActionTypeConfig = ActionTypeConfig, + Secrets extends ActionTypeSecrets = ActionTypeSecrets +> { + connectorId: string; + config: Config; + secrets: Secrets; + logger: Logger; + request: KibanaRequest; + services: HookServices; + isUpdate: boolean; +} + +export interface PostSaveConnectorHookParams< + Config extends ActionTypeConfig = ActionTypeConfig, + Secrets extends ActionTypeSecrets = ActionTypeSecrets +> { + connectorId: string; + config: Config; + secrets: Secrets; + logger: Logger; + request: KibanaRequest; + services: HookServices; + isUpdate: boolean; + wasSuccessful: boolean; +} + +export interface PostDeleteConnectorHookParams< + Config extends ActionTypeConfig = ActionTypeConfig, + Secrets extends ActionTypeSecrets = ActionTypeSecrets +> { + connectorId: string; + config: Config; + logger: Logger; + request: KibanaRequest; + services: HookServices; +} + export interface ActionType< Config extends ActionTypeConfig = ActionTypeConfig, Secrets extends ActionTypeSecrets = ActionTypeSecrets, @@ -171,6 +214,9 @@ export interface ActionType< renderParameterTemplates?: RenderParameterTemplates; executor: ExecutorType; getService?: (params: ServiceParams) => SubActionConnector; + preSaveHook?: (params: PreSaveConnectorHookParams) => Promise; + postSaveHook?: (params: PostSaveConnectorHookParams) => Promise; + postDeleteHook?: (params: PostDeleteConnectorHookParams) => Promise; } export interface RawAction extends Record { diff --git a/x-pack/plugins/actions/server/usage/actions_telemetry.test.ts b/x-pack/plugins/actions/server/usage/actions_telemetry.test.ts index b4f6d785584a4..26c37b36566e4 100644 --- a/x-pack/plugins/actions/server/usage/actions_telemetry.test.ts +++ b/x-pack/plugins/actions/server/usage/actions_telemetry.test.ts @@ -1025,15 +1025,17 @@ describe('actions telemetry', () => { '.d3security': 2, '.gen-ai__Azure OpenAI': 3, '.gen-ai__OpenAI': 1, + '.gen-ai__Other': 1, }; const { countByType, countGenAiProviderTypes } = getCounts(aggs); expect(countByType).toEqual({ __d3security: 2, - '__gen-ai': 4, + '__gen-ai': 5, }); expect(countGenAiProviderTypes).toEqual({ 'Azure OpenAI': 3, OpenAI: 1, + Other: 1, }); }); }); diff --git a/x-pack/plugins/actions/server/usage/types.ts b/x-pack/plugins/actions/server/usage/types.ts index d9fe796c2b4e0..6bdfe316c76e2 100644 --- a/x-pack/plugins/actions/server/usage/types.ts +++ b/x-pack/plugins/actions/server/usage/types.ts @@ -51,6 +51,7 @@ export const byGenAiProviderTypeSchema: MakeSchemaFrom['count_by_t // Known providers: ['Azure OpenAI']: { type: 'long' }, ['OpenAI']: { type: 'long' }, + ['Other']: { type: 'long' }, }; export const byServiceProviderTypeSchema: MakeSchemaFrom['count_active_email_connectors_by_service_type'] = diff --git a/x-pack/plugins/alerting/common/rules_settings.ts b/x-pack/plugins/alerting/common/rules_settings.ts index 2a4162ca2c5d3..6dcfd377eeb7c 100644 --- a/x-pack/plugins/alerting/common/rules_settings.ts +++ b/x-pack/plugins/alerting/common/rules_settings.ts @@ -5,38 +5,28 @@ * 2.0. */ -export interface RulesSettingsModificationMetadata { - createdBy: string | null; - updatedBy: string | null; - createdAt: string; - updatedAt: string; -} +import type { + RulesSettingsFlappingProperties, + RulesSettingsQueryDelayProperties, +} from '@kbn/alerting-types'; -export interface RulesSettingsFlappingProperties { - enabled: boolean; - lookBackWindow: number; - statusChangeThreshold: number; -} +export { + MIN_LOOK_BACK_WINDOW, + MAX_LOOK_BACK_WINDOW, + MIN_STATUS_CHANGE_THRESHOLD, + MAX_STATUS_CHANGE_THRESHOLD, +} from '@kbn/alerting-types/flapping/latest'; -export type RulesSettingsFlapping = RulesSettingsFlappingProperties & - RulesSettingsModificationMetadata; - -export interface RulesSettingsQueryDelayProperties { - delay: number; -} - -export type RulesSettingsQueryDelay = RulesSettingsQueryDelayProperties & - RulesSettingsModificationMetadata; - -export interface RulesSettingsProperties { - flapping?: RulesSettingsFlappingProperties; - queryDelay?: RulesSettingsQueryDelayProperties; -} - -export interface RulesSettings { - flapping?: RulesSettingsFlapping; - queryDelay?: RulesSettingsQueryDelay; -} +export type { + RulesSettingsModificationMetadata, + RulesSettingsFlappingProperties, + RulesSettingsQueryDelayProperties, + RuleSpecificFlappingProperties, + RulesSettingsFlapping, + RulesSettingsQueryDelay, + RulesSettingsProperties, + RulesSettings, +} from '@kbn/alerting-types'; export const MIN_QUERY_DELAY = 0; export const MAX_QUERY_DELAY = 60; diff --git a/x-pack/plugins/alerting/server/integration_tests/__snapshots__/serverless_upgrade_and_rollback_checks.test.ts.snap b/x-pack/plugins/alerting/server/integration_tests/__snapshots__/serverless_upgrade_and_rollback_checks.test.ts.snap index 4dc2abbc5f6a8..c283cc1087682 100644 --- a/x-pack/plugins/alerting/server/integration_tests/__snapshots__/serverless_upgrade_and_rollback_checks.test.ts.snap +++ b/x-pack/plugins/alerting/server/integration_tests/__snapshots__/serverless_upgrade_and_rollback_checks.test.ts.snap @@ -5829,6 +5829,175 @@ Object { }, "type": "array", }, + "responseActions": Object { + "items": Object { + "anyOf": Array [ + Object { + "additionalProperties": false, + "properties": Object { + "actionTypeId": Object { + "const": ".osquery", + "type": "string", + }, + "params": Object { + "additionalProperties": false, + "properties": Object { + "ecsMapping": Object { + "additionalProperties": Object { + "additionalProperties": false, + "properties": Object { + "field": Object { + "type": "string", + }, + "value": Object { + "anyOf": Array [ + Object { + "type": "string", + }, + Object { + "items": Object { + "type": "string", + }, + "type": "array", + }, + ], + }, + }, + "type": "object", + }, + "properties": Object {}, + "type": "object", + }, + "packId": Object { + "type": "string", + }, + "queries": Object { + "items": Object { + "additionalProperties": false, + "properties": Object { + "ecs_mapping": Object { + "$ref": "#/allOf/0/properties/responseActions/items/anyOf/0/properties/params/properties/ecsMapping", + }, + "id": Object { + "type": "string", + }, + "platform": Object { + "type": "string", + }, + "query": Object { + "type": "string", + }, + "removed": Object { + "type": "boolean", + }, + "snapshot": Object { + "type": "boolean", + }, + "version": Object { + "type": "string", + }, + }, + "required": Array [ + "id", + "query", + ], + "type": "object", + }, + "type": "array", + }, + "query": Object { + "type": "string", + }, + "savedQueryId": Object { + "type": "string", + }, + "timeout": Object { + "type": "number", + }, + }, + "type": "object", + }, + }, + "required": Array [ + "actionTypeId", + "params", + ], + "type": "object", + }, + Object { + "additionalProperties": false, + "properties": Object { + "actionTypeId": Object { + "const": ".endpoint", + "type": "string", + }, + "params": Object { + "anyOf": Array [ + Object { + "additionalProperties": false, + "properties": Object { + "command": Object { + "const": "isolate", + "type": "string", + }, + "comment": Object { + "type": "string", + }, + }, + "required": Array [ + "command", + ], + "type": "object", + }, + Object { + "additionalProperties": false, + "properties": Object { + "command": Object { + "enum": Array [ + "kill-process", + "suspend-process", + ], + "type": "string", + }, + "comment": Object { + "type": "string", + }, + "config": Object { + "additionalProperties": false, + "properties": Object { + "field": Object { + "type": "string", + }, + "overwrite": Object { + "default": true, + "type": "boolean", + }, + }, + "required": Array [ + "field", + ], + "type": "object", + }, + }, + "required": Array [ + "command", + "config", + ], + "type": "object", + }, + ], + }, + }, + "required": Array [ + "actionTypeId", + "params", + ], + "type": "object", + }, + ], + }, + "type": "array", + }, "riskScore": Object { "maximum": 100, "minimum": 0, @@ -6135,204 +6304,35 @@ Object { "query": Object { "type": "string", }, - "responseActions": Object { - "items": Object { - "anyOf": Array [ - Object { - "additionalProperties": false, - "properties": Object { - "actionTypeId": Object { - "const": ".osquery", - "type": "string", - }, - "params": Object { - "additionalProperties": false, - "properties": Object { - "ecsMapping": Object { - "additionalProperties": Object { - "additionalProperties": false, - "properties": Object { - "field": Object { - "type": "string", - }, - "value": Object { - "anyOf": Array [ - Object { - "type": "string", - }, - Object { - "items": Object { - "type": "string", - }, - "type": "array", - }, - ], - }, - }, - "type": "object", - }, - "properties": Object {}, - "type": "object", - }, - "packId": Object { - "type": "string", - }, - "queries": Object { - "items": Object { - "additionalProperties": false, - "properties": Object { - "ecs_mapping": Object { - "$ref": "#/allOf/1/properties/responseActions/items/anyOf/0/properties/params/properties/ecsMapping", - }, - "id": Object { - "type": "string", - }, - "platform": Object { - "type": "string", - }, - "query": Object { - "type": "string", - }, - "removed": Object { - "type": "boolean", - }, - "snapshot": Object { - "type": "boolean", - }, - "version": Object { - "type": "string", - }, - }, - "required": Array [ - "id", - "query", - ], - "type": "object", - }, - "type": "array", - }, - "query": Object { - "type": "string", - }, - "savedQueryId": Object { - "type": "string", - }, - "timeout": Object { - "type": "number", - }, - }, - "type": "object", - }, - }, - "required": Array [ - "actionTypeId", - "params", - ], - "type": "object", - }, - Object { - "additionalProperties": false, - "properties": Object { - "actionTypeId": Object { - "const": ".endpoint", - "type": "string", - }, - "params": Object { - "anyOf": Array [ - Object { - "additionalProperties": false, - "properties": Object { - "command": Object { - "const": "isolate", - "type": "string", - }, - "comment": Object { - "type": "string", - }, - }, - "required": Array [ - "command", - ], - "type": "object", - }, - Object { - "additionalProperties": false, - "properties": Object { - "command": Object { - "enum": Array [ - "kill-process", - "suspend-process", - ], - "type": "string", - }, - "comment": Object { - "type": "string", - }, - "config": Object { - "additionalProperties": false, - "properties": Object { - "field": Object { - "type": "string", - }, - "overwrite": Object { - "default": true, - "type": "boolean", - }, - }, - "required": Array [ - "field", - ], - "type": "object", - }, - }, - "required": Array [ - "command", - "config", - ], - "type": "object", - }, - ], - }, - }, - "required": Array [ - "actionTypeId", - "params", - ], - "type": "object", - }, - ], - }, - "type": "array", - }, - "tiebreakerField": Object { - "type": "string", - }, - "timestampField": Object { - "type": "string", - }, - "type": Object { - "const": "eql", - "type": "string", - }, - }, - "required": Array [ - "type", - "language", - "query", - ], - "type": "object", - }, - ], -} -`; - -exports[`Serverless upgrade and rollback checks detect param changes to review for: siem.indicatorRule 1`] = ` -Object { - "$schema": "http://json-schema.org/draft-07/schema#", - "allOf": Array [ - Object { - "properties": Object { - "author": Object { + "tiebreakerField": Object { + "type": "string", + }, + "timestampField": Object { + "type": "string", + }, + "type": Object { + "const": "eql", + "type": "string", + }, + }, + "required": Array [ + "type", + "language", + "query", + ], + "type": "object", + }, + ], +} +`; + +exports[`Serverless upgrade and rollback checks detect param changes to review for: siem.indicatorRule 1`] = ` +Object { + "$schema": "http://json-schema.org/draft-07/schema#", + "allOf": Array [ + Object { + "properties": Object { + "author": Object { "items": Object { "type": "string", }, @@ -6497,34 +6497,203 @@ Object { }, "type": "array", }, - "riskScore": Object { - "maximum": 100, - "minimum": 0, - "type": "integer", - }, - "riskScoreMapping": Object { + "responseActions": Object { "items": Object { - "additionalProperties": false, - "properties": Object { - "field": Object { - "type": "string", - }, - "operator": Object { - "const": "equals", - "type": "string", - }, - "risk_score": Object { - "$ref": "#/allOf/0/properties/riskScore", - }, - "value": Object { - "type": "string", + "anyOf": Array [ + Object { + "additionalProperties": false, + "properties": Object { + "actionTypeId": Object { + "const": ".osquery", + "type": "string", + }, + "params": Object { + "additionalProperties": false, + "properties": Object { + "ecsMapping": Object { + "additionalProperties": Object { + "additionalProperties": false, + "properties": Object { + "field": Object { + "type": "string", + }, + "value": Object { + "anyOf": Array [ + Object { + "type": "string", + }, + Object { + "items": Object { + "type": "string", + }, + "type": "array", + }, + ], + }, + }, + "type": "object", + }, + "properties": Object {}, + "type": "object", + }, + "packId": Object { + "type": "string", + }, + "queries": Object { + "items": Object { + "additionalProperties": false, + "properties": Object { + "ecs_mapping": Object { + "$ref": "#/allOf/0/properties/responseActions/items/anyOf/0/properties/params/properties/ecsMapping", + }, + "id": Object { + "type": "string", + }, + "platform": Object { + "type": "string", + }, + "query": Object { + "type": "string", + }, + "removed": Object { + "type": "boolean", + }, + "snapshot": Object { + "type": "boolean", + }, + "version": Object { + "type": "string", + }, + }, + "required": Array [ + "id", + "query", + ], + "type": "object", + }, + "type": "array", + }, + "query": Object { + "type": "string", + }, + "savedQueryId": Object { + "type": "string", + }, + "timeout": Object { + "type": "number", + }, + }, + "type": "object", + }, + }, + "required": Array [ + "actionTypeId", + "params", + ], + "type": "object", }, - }, - "required": Array [ - "field", - "operator", - "value", - ], + Object { + "additionalProperties": false, + "properties": Object { + "actionTypeId": Object { + "const": ".endpoint", + "type": "string", + }, + "params": Object { + "anyOf": Array [ + Object { + "additionalProperties": false, + "properties": Object { + "command": Object { + "const": "isolate", + "type": "string", + }, + "comment": Object { + "type": "string", + }, + }, + "required": Array [ + "command", + ], + "type": "object", + }, + Object { + "additionalProperties": false, + "properties": Object { + "command": Object { + "enum": Array [ + "kill-process", + "suspend-process", + ], + "type": "string", + }, + "comment": Object { + "type": "string", + }, + "config": Object { + "additionalProperties": false, + "properties": Object { + "field": Object { + "type": "string", + }, + "overwrite": Object { + "default": true, + "type": "boolean", + }, + }, + "required": Array [ + "field", + ], + "type": "object", + }, + }, + "required": Array [ + "command", + "config", + ], + "type": "object", + }, + ], + }, + }, + "required": Array [ + "actionTypeId", + "params", + ], + "type": "object", + }, + ], + }, + "type": "array", + }, + "riskScore": Object { + "maximum": 100, + "minimum": 0, + "type": "integer", + }, + "riskScoreMapping": Object { + "items": Object { + "additionalProperties": false, + "properties": Object { + "field": Object { + "type": "string", + }, + "operator": Object { + "const": "equals", + "type": "string", + }, + "risk_score": Object { + "$ref": "#/allOf/0/properties/riskScore", + }, + "value": Object { + "type": "string", + }, + }, + "required": Array [ + "field", + "operator", + "value", + ], "type": "object", }, "type": "array", @@ -7059,483 +7228,172 @@ Object { }, "type": "array", }, - "riskScore": Object { - "maximum": 100, - "minimum": 0, - "type": "integer", - }, - "riskScoreMapping": Object { - "items": Object { - "additionalProperties": false, - "properties": Object { - "field": Object { - "type": "string", - }, - "operator": Object { - "const": "equals", - "type": "string", - }, - "risk_score": Object { - "$ref": "#/allOf/0/properties/riskScore", - }, - "value": Object { - "type": "string", - }, - }, - "required": Array [ - "field", - "operator", - "value", - ], - "type": "object", - }, - "type": "array", - }, - "ruleId": Object { - "type": "string", - }, - "ruleNameOverride": Object { - "type": "string", - }, - "ruleSource": Object { - "anyOf": Array [ - Object { - "additionalProperties": false, - "properties": Object { - "isCustomized": Object { - "type": "boolean", - }, - "type": Object { - "const": "external", - "type": "string", - }, - }, - "required": Array [ - "type", - "isCustomized", - ], - "type": "object", - }, - Object { - "additionalProperties": false, - "properties": Object { - "type": Object { - "const": "internal", - "type": "string", - }, - }, - "required": Array [ - "type", - ], - "type": "object", - }, - ], - }, - "setup": Object { - "type": "string", - }, - "severity": Object { - "enum": Array [ - "low", - "medium", - "high", - "critical", - ], - "type": "string", - }, - "severityMapping": Object { - "items": Object { - "additionalProperties": false, - "properties": Object { - "field": Object { - "type": "string", - }, - "operator": Object { - "const": "equals", - "type": "string", - }, - "severity": Object { - "$ref": "#/allOf/0/properties/severity", - }, - "value": Object { - "type": "string", - }, - }, - "required": Array [ - "field", - "operator", - "severity", - "value", - ], - "type": "object", - }, - "type": "array", - }, - "threat": Object { + "responseActions": Object { "items": Object { - "additionalProperties": false, - "properties": Object { - "framework": Object { - "type": "string", - }, - "tactic": Object { + "anyOf": Array [ + Object { "additionalProperties": false, "properties": Object { - "id": Object { - "type": "string", - }, - "name": Object { - "type": "string", - }, - "reference": Object { + "actionTypeId": Object { + "const": ".osquery", "type": "string", }, - }, - "required": Array [ - "id", - "name", - "reference", - ], - "type": "object", - }, - "technique": Object { - "items": Object { - "additionalProperties": false, - "properties": Object { - "id": Object { - "type": "string", - }, - "name": Object { - "type": "string", - }, - "reference": Object { - "type": "string", - }, - "subtechnique": Object { - "items": Object { + "params": Object { + "additionalProperties": false, + "properties": Object { + "ecsMapping": Object { + "additionalProperties": Object { + "additionalProperties": false, + "properties": Object { + "field": Object { + "type": "string", + }, + "value": Object { + "anyOf": Array [ + Object { + "type": "string", + }, + Object { + "items": Object { + "type": "string", + }, + "type": "array", + }, + ], + }, + }, + "type": "object", + }, + "properties": Object {}, + "type": "object", + }, + "packId": Object { + "type": "string", + }, + "queries": Object { + "items": Object { + "additionalProperties": false, + "properties": Object { + "ecs_mapping": Object { + "$ref": "#/allOf/0/properties/responseActions/items/anyOf/0/properties/params/properties/ecsMapping", + }, + "id": Object { + "type": "string", + }, + "platform": Object { + "type": "string", + }, + "query": Object { + "type": "string", + }, + "removed": Object { + "type": "boolean", + }, + "snapshot": Object { + "type": "boolean", + }, + "version": Object { + "type": "string", + }, + }, + "required": Array [ + "id", + "query", + ], + "type": "object", + }, + "type": "array", + }, + "query": Object { + "type": "string", + }, + "savedQueryId": Object { + "type": "string", + }, + "timeout": Object { + "type": "number", + }, + }, + "type": "object", + }, + }, + "required": Array [ + "actionTypeId", + "params", + ], + "type": "object", + }, + Object { + "additionalProperties": false, + "properties": Object { + "actionTypeId": Object { + "const": ".endpoint", + "type": "string", + }, + "params": Object { + "anyOf": Array [ + Object { "additionalProperties": false, "properties": Object { - "id": Object { - "type": "string", - }, - "name": Object { + "command": Object { + "const": "isolate", "type": "string", }, - "reference": Object { + "comment": Object { "type": "string", }, }, "required": Array [ - "id", - "name", - "reference", + "command", ], "type": "object", }, - "type": "array", - }, - }, - "required": Array [ - "id", - "name", - "reference", - ], - "type": "object", - }, - "type": "array", - }, - }, - "required": Array [ - "framework", - "tactic", - ], - "type": "object", - }, - "type": "array", - }, - "timelineId": Object { - "type": "string", - }, - "timelineTitle": Object { - "type": "string", - }, - "timestampOverride": Object { - "type": "string", - }, - "timestampOverrideFallbackDisabled": Object { - "type": "boolean", - }, - "to": Object { - "type": "string", - }, - "version": Object { - "minimum": 1, - "type": "integer", - }, - }, - "required": Array [ - "author", - "description", - "falsePositives", - "from", - "ruleId", - "immutable", - "outputIndex", - "maxSignals", - "riskScore", - "riskScoreMapping", - "severity", - "severityMapping", - "threat", - "to", - "references", - "version", - "exceptionsList", - ], - "type": "object", - }, - Object { - "properties": Object { - "alertSuppression": Object { - "additionalProperties": false, - "properties": Object { - "duration": Object { - "additionalProperties": false, - "properties": Object { - "unit": Object { - "enum": Array [ - "s", - "m", - "h", - ], - "type": "string", - }, - "value": Object { - "minimum": 1, - "type": "integer", - }, - }, - "required": Array [ - "value", - "unit", - ], - "type": "object", - }, - "groupBy": Object { - "items": Object { - "type": "string", - }, - "maxItems": 3, - "minItems": 1, - "type": "array", - }, - "missingFieldsStrategy": Object { - "enum": Array [ - "doNotSuppress", - "suppress", - ], - "type": "string", - }, - }, - "required": Array [ - "groupBy", - ], - "type": "object", - }, - "anomalyThreshold": Object { - "minimum": 0, - "type": "integer", - }, - "machineLearningJobId": Object { - "items": Object { - "type": "string", - }, - "type": "array", - }, - "type": Object { - "const": "machine_learning", - "type": "string", - }, - }, - "required": Array [ - "type", - "anomalyThreshold", - "machineLearningJobId", - ], - "type": "object", - }, - ], -} -`; - -exports[`Serverless upgrade and rollback checks detect param changes to review for: siem.newTermsRule 1`] = ` -Object { - "$schema": "http://json-schema.org/draft-07/schema#", - "allOf": Array [ - Object { - "properties": Object { - "author": Object { - "items": Object { - "type": "string", - }, - "type": "array", - }, - "buildingBlockType": Object { - "type": "string", - }, - "description": Object { - "minLength": 1, - "type": "string", - }, - "exceptionsList": Object { - "items": Object { - "additionalProperties": false, - "properties": Object { - "id": Object { - "$ref": "#/allOf/0/properties/investigationFields/anyOf/0/properties/field_names/items", - }, - "list_id": Object { - "$ref": "#/allOf/0/properties/investigationFields/anyOf/0/properties/field_names/items", - }, - "namespace_type": Object { - "enum": Array [ - "agnostic", - "single", - ], - "type": "string", - }, - "type": Object { - "enum": Array [ - "detection", - "rule_default", - "endpoint", - "endpoint_trusted_apps", - "endpoint_events", - "endpoint_host_isolation_exceptions", - "endpoint_blocklists", - ], - "type": "string", - }, - }, - "required": Array [ - "id", - "list_id", - "type", - "namespace_type", - ], - "type": "object", - }, - "type": "array", - }, - "falsePositives": Object { - "items": Object { - "type": "string", - }, - "type": "array", - }, - "from": Object { - "type": "string", - }, - "immutable": Object { - "type": "boolean", - }, - "investigationFields": Object { - "anyOf": Array [ - Object { - "additionalProperties": false, - "properties": Object { - "field_names": Object { - "items": Object { - "minLength": 1, - "pattern": "^(?! *$).+$", - "type": "string", + Object { + "additionalProperties": false, + "properties": Object { + "command": Object { + "enum": Array [ + "kill-process", + "suspend-process", + ], + "type": "string", + }, + "comment": Object { + "type": "string", + }, + "config": Object { + "additionalProperties": false, + "properties": Object { + "field": Object { + "type": "string", + }, + "overwrite": Object { + "default": true, + "type": "boolean", + }, + }, + "required": Array [ + "field", + ], + "type": "object", + }, + }, + "required": Array [ + "command", + "config", + ], + "type": "object", + }, + ], }, - "minItems": 1, - "type": "array", }, + "required": Array [ + "actionTypeId", + "params", + ], + "type": "object", }, - "required": Array [ - "field_names", - ], - "type": "object", - }, - Object { - "items": Object { - "type": "string", - }, - "type": "array", - }, - ], - }, - "license": Object { - "type": "string", - }, - "maxSignals": Object { - "minimum": 1, - "type": "integer", - }, - "meta": Object { - "additionalProperties": Object {}, - "properties": Object {}, - "type": "object", - }, - "namespace": Object { - "type": "string", - }, - "note": Object { - "type": "string", - }, - "outputIndex": Object { - "type": "string", - }, - "references": Object { - "items": Object { - "type": "string", - }, - "type": "array", - }, - "relatedIntegrations": Object { - "items": Object { - "additionalProperties": false, - "properties": Object { - "integration": Object { - "$ref": "#/allOf/0/properties/investigationFields/anyOf/0/properties/field_names/items", - }, - "package": Object { - "$ref": "#/allOf/0/properties/investigationFields/anyOf/0/properties/field_names/items", - }, - "version": Object { - "$ref": "#/allOf/0/properties/investigationFields/anyOf/0/properties/field_names/items", - }, - }, - "required": Array [ - "package", - "version", - ], - "type": "object", - }, - "type": "array", - }, - "requiredFields": Object { - "items": Object { - "additionalProperties": false, - "properties": Object { - "ecs": Object { - "type": "boolean", - }, - "name": Object { - "$ref": "#/allOf/0/properties/investigationFields/anyOf/0/properties/field_names/items", - }, - "type": Object { - "$ref": "#/allOf/0/properties/investigationFields/anyOf/0/properties/field_names/items", - }, - }, - "required": Array [ - "name", - "type", - "ecs", ], - "type": "object", }, "type": "array", }, @@ -7822,39 +7680,202 @@ Object { ], "type": "object", }, - "dataViewId": Object { + "anomalyThreshold": Object { + "minimum": 0, + "type": "integer", + }, + "machineLearningJobId": Object { + "items": Object { + "type": "string", + }, + "type": "array", + }, + "type": Object { + "const": "machine_learning", "type": "string", }, - "filters": Object { - "items": Object {}, + }, + "required": Array [ + "type", + "anomalyThreshold", + "machineLearningJobId", + ], + "type": "object", + }, + ], +} +`; + +exports[`Serverless upgrade and rollback checks detect param changes to review for: siem.newTermsRule 1`] = ` +Object { + "$schema": "http://json-schema.org/draft-07/schema#", + "allOf": Array [ + Object { + "properties": Object { + "author": Object { + "items": Object { + "type": "string", + }, "type": "array", }, - "historyWindowStart": Object { - "$ref": "#/allOf/0/properties/investigationFields/anyOf/0/properties/field_names/items", + "buildingBlockType": Object { + "type": "string", }, - "index": Object { + "description": Object { + "minLength": 1, + "type": "string", + }, + "exceptionsList": Object { + "items": Object { + "additionalProperties": false, + "properties": Object { + "id": Object { + "$ref": "#/allOf/0/properties/investigationFields/anyOf/0/properties/field_names/items", + }, + "list_id": Object { + "$ref": "#/allOf/0/properties/investigationFields/anyOf/0/properties/field_names/items", + }, + "namespace_type": Object { + "enum": Array [ + "agnostic", + "single", + ], + "type": "string", + }, + "type": Object { + "enum": Array [ + "detection", + "rule_default", + "endpoint", + "endpoint_trusted_apps", + "endpoint_events", + "endpoint_host_isolation_exceptions", + "endpoint_blocklists", + ], + "type": "string", + }, + }, + "required": Array [ + "id", + "list_id", + "type", + "namespace_type", + ], + "type": "object", + }, + "type": "array", + }, + "falsePositives": Object { "items": Object { "type": "string", }, "type": "array", }, - "language": Object { - "enum": Array [ - "kuery", - "lucene", + "from": Object { + "type": "string", + }, + "immutable": Object { + "type": "boolean", + }, + "investigationFields": Object { + "anyOf": Array [ + Object { + "additionalProperties": false, + "properties": Object { + "field_names": Object { + "items": Object { + "minLength": 1, + "pattern": "^(?! *$).+$", + "type": "string", + }, + "minItems": 1, + "type": "array", + }, + }, + "required": Array [ + "field_names", + ], + "type": "object", + }, + Object { + "items": Object { + "type": "string", + }, + "type": "array", + }, ], + }, + "license": Object { + "type": "string", + }, + "maxSignals": Object { + "minimum": 1, + "type": "integer", + }, + "meta": Object { + "additionalProperties": Object {}, + "properties": Object {}, + "type": "object", + }, + "namespace": Object { + "type": "string", + }, + "note": Object { + "type": "string", + }, + "outputIndex": Object { "type": "string", }, - "newTermsFields": Object { + "references": Object { "items": Object { "type": "string", }, - "maxItems": 3, - "minItems": 1, "type": "array", }, - "query": Object { - "type": "string", + "relatedIntegrations": Object { + "items": Object { + "additionalProperties": false, + "properties": Object { + "integration": Object { + "$ref": "#/allOf/0/properties/investigationFields/anyOf/0/properties/field_names/items", + }, + "package": Object { + "$ref": "#/allOf/0/properties/investigationFields/anyOf/0/properties/field_names/items", + }, + "version": Object { + "$ref": "#/allOf/0/properties/investigationFields/anyOf/0/properties/field_names/items", + }, + }, + "required": Array [ + "package", + "version", + ], + "type": "object", + }, + "type": "array", + }, + "requiredFields": Object { + "items": Object { + "additionalProperties": false, + "properties": Object { + "ecs": Object { + "type": "boolean", + }, + "name": Object { + "$ref": "#/allOf/0/properties/investigationFields/anyOf/0/properties/field_names/items", + }, + "type": Object { + "$ref": "#/allOf/0/properties/investigationFields/anyOf/0/properties/field_names/items", + }, + }, + "required": Array [ + "name", + "type", + "ecs", + ], + "type": "object", + }, + "type": "array", }, "responseActions": Object { "items": Object { @@ -7903,7 +7924,7 @@ Object { "additionalProperties": false, "properties": Object { "ecs_mapping": Object { - "$ref": "#/allOf/1/properties/responseActions/items/anyOf/0/properties/params/properties/ecsMapping", + "$ref": "#/allOf/0/properties/responseActions/items/anyOf/0/properties/params/properties/ecsMapping", }, "id": Object { "type": "string", @@ -8015,16 +8036,333 @@ Object { ], }, }, - "required": Array [ - "actionTypeId", - "params", - ], - "type": "object", + "required": Array [ + "actionTypeId", + "params", + ], + "type": "object", + }, + ], + }, + "type": "array", + }, + "riskScore": Object { + "maximum": 100, + "minimum": 0, + "type": "integer", + }, + "riskScoreMapping": Object { + "items": Object { + "additionalProperties": false, + "properties": Object { + "field": Object { + "type": "string", + }, + "operator": Object { + "const": "equals", + "type": "string", + }, + "risk_score": Object { + "$ref": "#/allOf/0/properties/riskScore", + }, + "value": Object { + "type": "string", + }, + }, + "required": Array [ + "field", + "operator", + "value", + ], + "type": "object", + }, + "type": "array", + }, + "ruleId": Object { + "type": "string", + }, + "ruleNameOverride": Object { + "type": "string", + }, + "ruleSource": Object { + "anyOf": Array [ + Object { + "additionalProperties": false, + "properties": Object { + "isCustomized": Object { + "type": "boolean", + }, + "type": Object { + "const": "external", + "type": "string", + }, + }, + "required": Array [ + "type", + "isCustomized", + ], + "type": "object", + }, + Object { + "additionalProperties": false, + "properties": Object { + "type": Object { + "const": "internal", + "type": "string", + }, + }, + "required": Array [ + "type", + ], + "type": "object", + }, + ], + }, + "setup": Object { + "type": "string", + }, + "severity": Object { + "enum": Array [ + "low", + "medium", + "high", + "critical", + ], + "type": "string", + }, + "severityMapping": Object { + "items": Object { + "additionalProperties": false, + "properties": Object { + "field": Object { + "type": "string", + }, + "operator": Object { + "const": "equals", + "type": "string", + }, + "severity": Object { + "$ref": "#/allOf/0/properties/severity", + }, + "value": Object { + "type": "string", + }, + }, + "required": Array [ + "field", + "operator", + "severity", + "value", + ], + "type": "object", + }, + "type": "array", + }, + "threat": Object { + "items": Object { + "additionalProperties": false, + "properties": Object { + "framework": Object { + "type": "string", + }, + "tactic": Object { + "additionalProperties": false, + "properties": Object { + "id": Object { + "type": "string", + }, + "name": Object { + "type": "string", + }, + "reference": Object { + "type": "string", + }, + }, + "required": Array [ + "id", + "name", + "reference", + ], + "type": "object", + }, + "technique": Object { + "items": Object { + "additionalProperties": false, + "properties": Object { + "id": Object { + "type": "string", + }, + "name": Object { + "type": "string", + }, + "reference": Object { + "type": "string", + }, + "subtechnique": Object { + "items": Object { + "additionalProperties": false, + "properties": Object { + "id": Object { + "type": "string", + }, + "name": Object { + "type": "string", + }, + "reference": Object { + "type": "string", + }, + }, + "required": Array [ + "id", + "name", + "reference", + ], + "type": "object", + }, + "type": "array", + }, + }, + "required": Array [ + "id", + "name", + "reference", + ], + "type": "object", + }, + "type": "array", + }, + }, + "required": Array [ + "framework", + "tactic", + ], + "type": "object", + }, + "type": "array", + }, + "timelineId": Object { + "type": "string", + }, + "timelineTitle": Object { + "type": "string", + }, + "timestampOverride": Object { + "type": "string", + }, + "timestampOverrideFallbackDisabled": Object { + "type": "boolean", + }, + "to": Object { + "type": "string", + }, + "version": Object { + "minimum": 1, + "type": "integer", + }, + }, + "required": Array [ + "author", + "description", + "falsePositives", + "from", + "ruleId", + "immutable", + "outputIndex", + "maxSignals", + "riskScore", + "riskScoreMapping", + "severity", + "severityMapping", + "threat", + "to", + "references", + "version", + "exceptionsList", + ], + "type": "object", + }, + Object { + "properties": Object { + "alertSuppression": Object { + "additionalProperties": false, + "properties": Object { + "duration": Object { + "additionalProperties": false, + "properties": Object { + "unit": Object { + "enum": Array [ + "s", + "m", + "h", + ], + "type": "string", + }, + "value": Object { + "minimum": 1, + "type": "integer", + }, }, - ], + "required": Array [ + "value", + "unit", + ], + "type": "object", + }, + "groupBy": Object { + "items": Object { + "type": "string", + }, + "maxItems": 3, + "minItems": 1, + "type": "array", + }, + "missingFieldsStrategy": Object { + "enum": Array [ + "doNotSuppress", + "suppress", + ], + "type": "string", + }, + }, + "required": Array [ + "groupBy", + ], + "type": "object", + }, + "dataViewId": Object { + "type": "string", + }, + "filters": Object { + "items": Object {}, + "type": "array", + }, + "historyWindowStart": Object { + "$ref": "#/allOf/0/properties/investigationFields/anyOf/0/properties/field_names/items", + }, + "index": Object { + "items": Object { + "type": "string", + }, + "type": "array", + }, + "language": Object { + "enum": Array [ + "kuery", + "lucene", + ], + "type": "string", + }, + "newTermsFields": Object { + "items": Object { + "type": "string", }, + "maxItems": 3, + "minItems": 1, "type": "array", }, + "query": Object { + "type": "string", + }, "type": Object { "const": "new_terms", "type": "string", @@ -8233,13 +8571,182 @@ Object { "type": Object { "$ref": "#/allOf/0/properties/investigationFields/anyOf/0/properties/field_names/items", }, - }, - "required": Array [ - "name", - "type", - "ecs", + }, + "required": Array [ + "name", + "type", + "ecs", + ], + "type": "object", + }, + "type": "array", + }, + "responseActions": Object { + "items": Object { + "anyOf": Array [ + Object { + "additionalProperties": false, + "properties": Object { + "actionTypeId": Object { + "const": ".osquery", + "type": "string", + }, + "params": Object { + "additionalProperties": false, + "properties": Object { + "ecsMapping": Object { + "additionalProperties": Object { + "additionalProperties": false, + "properties": Object { + "field": Object { + "type": "string", + }, + "value": Object { + "anyOf": Array [ + Object { + "type": "string", + }, + Object { + "items": Object { + "type": "string", + }, + "type": "array", + }, + ], + }, + }, + "type": "object", + }, + "properties": Object {}, + "type": "object", + }, + "packId": Object { + "type": "string", + }, + "queries": Object { + "items": Object { + "additionalProperties": false, + "properties": Object { + "ecs_mapping": Object { + "$ref": "#/allOf/0/properties/responseActions/items/anyOf/0/properties/params/properties/ecsMapping", + }, + "id": Object { + "type": "string", + }, + "platform": Object { + "type": "string", + }, + "query": Object { + "type": "string", + }, + "removed": Object { + "type": "boolean", + }, + "snapshot": Object { + "type": "boolean", + }, + "version": Object { + "type": "string", + }, + }, + "required": Array [ + "id", + "query", + ], + "type": "object", + }, + "type": "array", + }, + "query": Object { + "type": "string", + }, + "savedQueryId": Object { + "type": "string", + }, + "timeout": Object { + "type": "number", + }, + }, + "type": "object", + }, + }, + "required": Array [ + "actionTypeId", + "params", + ], + "type": "object", + }, + Object { + "additionalProperties": false, + "properties": Object { + "actionTypeId": Object { + "const": ".endpoint", + "type": "string", + }, + "params": Object { + "anyOf": Array [ + Object { + "additionalProperties": false, + "properties": Object { + "command": Object { + "const": "isolate", + "type": "string", + }, + "comment": Object { + "type": "string", + }, + }, + "required": Array [ + "command", + ], + "type": "object", + }, + Object { + "additionalProperties": false, + "properties": Object { + "command": Object { + "enum": Array [ + "kill-process", + "suspend-process", + ], + "type": "string", + }, + "comment": Object { + "type": "string", + }, + "config": Object { + "additionalProperties": false, + "properties": Object { + "field": Object { + "type": "string", + }, + "overwrite": Object { + "default": true, + "type": "boolean", + }, + }, + "required": Array [ + "field", + ], + "type": "object", + }, + }, + "required": Array [ + "command", + "config", + ], + "type": "object", + }, + ], + }, + }, + "required": Array [ + "actionTypeId", + "params", + ], + "type": "object", + }, ], - "type": "object", }, "type": "array", }, @@ -8552,175 +9059,6 @@ Object { "query": Object { "type": "string", }, - "responseActions": Object { - "items": Object { - "anyOf": Array [ - Object { - "additionalProperties": false, - "properties": Object { - "actionTypeId": Object { - "const": ".osquery", - "type": "string", - }, - "params": Object { - "additionalProperties": false, - "properties": Object { - "ecsMapping": Object { - "additionalProperties": Object { - "additionalProperties": false, - "properties": Object { - "field": Object { - "type": "string", - }, - "value": Object { - "anyOf": Array [ - Object { - "type": "string", - }, - Object { - "items": Object { - "type": "string", - }, - "type": "array", - }, - ], - }, - }, - "type": "object", - }, - "properties": Object {}, - "type": "object", - }, - "packId": Object { - "type": "string", - }, - "queries": Object { - "items": Object { - "additionalProperties": false, - "properties": Object { - "ecs_mapping": Object { - "$ref": "#/allOf/1/anyOf/0/properties/responseActions/items/anyOf/0/properties/params/properties/ecsMapping", - }, - "id": Object { - "type": "string", - }, - "platform": Object { - "type": "string", - }, - "query": Object { - "type": "string", - }, - "removed": Object { - "type": "boolean", - }, - "snapshot": Object { - "type": "boolean", - }, - "version": Object { - "type": "string", - }, - }, - "required": Array [ - "id", - "query", - ], - "type": "object", - }, - "type": "array", - }, - "query": Object { - "type": "string", - }, - "savedQueryId": Object { - "type": "string", - }, - "timeout": Object { - "type": "number", - }, - }, - "type": "object", - }, - }, - "required": Array [ - "actionTypeId", - "params", - ], - "type": "object", - }, - Object { - "additionalProperties": false, - "properties": Object { - "actionTypeId": Object { - "const": ".endpoint", - "type": "string", - }, - "params": Object { - "anyOf": Array [ - Object { - "additionalProperties": false, - "properties": Object { - "command": Object { - "const": "isolate", - "type": "string", - }, - "comment": Object { - "type": "string", - }, - }, - "required": Array [ - "command", - ], - "type": "object", - }, - Object { - "additionalProperties": false, - "properties": Object { - "command": Object { - "enum": Array [ - "kill-process", - "suspend-process", - ], - "type": "string", - }, - "comment": Object { - "type": "string", - }, - "config": Object { - "additionalProperties": false, - "properties": Object { - "field": Object { - "type": "string", - }, - "overwrite": Object { - "default": true, - "type": "boolean", - }, - }, - "required": Array [ - "field", - ], - "type": "object", - }, - }, - "required": Array [ - "command", - "config", - ], - "type": "object", - }, - ], - }, - }, - "required": Array [ - "actionTypeId", - "params", - ], - "type": "object", - }, - ], - }, - "type": "array", - }, "savedId": Object { "type": "string", }, @@ -8757,12 +9095,6 @@ Object { "query": Object { "$ref": "#/allOf/1/anyOf/0/properties/query", }, - "responseActions": Object { - "items": Object { - "$ref": "#/allOf/1/anyOf/0/properties/responseActions/items", - }, - "type": "array", - }, "savedId": Object { "$ref": "#/allOf/1/anyOf/0/properties/savedId", }, @@ -8942,16 +9274,185 @@ Object { "name": Object { "$ref": "#/allOf/0/properties/investigationFields/anyOf/0/properties/field_names/items", }, - "type": Object { - "$ref": "#/allOf/0/properties/investigationFields/anyOf/0/properties/field_names/items", + "type": Object { + "$ref": "#/allOf/0/properties/investigationFields/anyOf/0/properties/field_names/items", + }, + }, + "required": Array [ + "name", + "type", + "ecs", + ], + "type": "object", + }, + "type": "array", + }, + "responseActions": Object { + "items": Object { + "anyOf": Array [ + Object { + "additionalProperties": false, + "properties": Object { + "actionTypeId": Object { + "const": ".osquery", + "type": "string", + }, + "params": Object { + "additionalProperties": false, + "properties": Object { + "ecsMapping": Object { + "additionalProperties": Object { + "additionalProperties": false, + "properties": Object { + "field": Object { + "type": "string", + }, + "value": Object { + "anyOf": Array [ + Object { + "type": "string", + }, + Object { + "items": Object { + "type": "string", + }, + "type": "array", + }, + ], + }, + }, + "type": "object", + }, + "properties": Object {}, + "type": "object", + }, + "packId": Object { + "type": "string", + }, + "queries": Object { + "items": Object { + "additionalProperties": false, + "properties": Object { + "ecs_mapping": Object { + "$ref": "#/allOf/0/properties/responseActions/items/anyOf/0/properties/params/properties/ecsMapping", + }, + "id": Object { + "type": "string", + }, + "platform": Object { + "type": "string", + }, + "query": Object { + "type": "string", + }, + "removed": Object { + "type": "boolean", + }, + "snapshot": Object { + "type": "boolean", + }, + "version": Object { + "type": "string", + }, + }, + "required": Array [ + "id", + "query", + ], + "type": "object", + }, + "type": "array", + }, + "query": Object { + "type": "string", + }, + "savedQueryId": Object { + "type": "string", + }, + "timeout": Object { + "type": "number", + }, + }, + "type": "object", + }, + }, + "required": Array [ + "actionTypeId", + "params", + ], + "type": "object", + }, + Object { + "additionalProperties": false, + "properties": Object { + "actionTypeId": Object { + "const": ".endpoint", + "type": "string", + }, + "params": Object { + "anyOf": Array [ + Object { + "additionalProperties": false, + "properties": Object { + "command": Object { + "const": "isolate", + "type": "string", + }, + "comment": Object { + "type": "string", + }, + }, + "required": Array [ + "command", + ], + "type": "object", + }, + Object { + "additionalProperties": false, + "properties": Object { + "command": Object { + "enum": Array [ + "kill-process", + "suspend-process", + ], + "type": "string", + }, + "comment": Object { + "type": "string", + }, + "config": Object { + "additionalProperties": false, + "properties": Object { + "field": Object { + "type": "string", + }, + "overwrite": Object { + "default": true, + "type": "boolean", + }, + }, + "required": Array [ + "field", + ], + "type": "object", + }, + }, + "required": Array [ + "command", + "config", + ], + "type": "object", + }, + ], + }, + }, + "required": Array [ + "actionTypeId", + "params", + ], + "type": "object", }, - }, - "required": Array [ - "name", - "type", - "ecs", ], - "type": "object", }, "type": "array", }, @@ -9178,261 +9679,92 @@ Object { "immutable", "outputIndex", "maxSignals", - "riskScore", - "riskScoreMapping", - "severity", - "severityMapping", - "threat", - "to", - "references", - "version", - "exceptionsList", - ], - "type": "object", - }, - Object { - "anyOf": Array [ - Object { - "additionalProperties": false, - "properties": Object { - "alertSuppression": Object { - "additionalProperties": false, - "properties": Object { - "duration": Object { - "additionalProperties": false, - "properties": Object { - "unit": Object { - "enum": Array [ - "s", - "m", - "h", - ], - "type": "string", - }, - "value": Object { - "minimum": 1, - "type": "integer", - }, - }, - "required": Array [ - "value", - "unit", - ], - "type": "object", - }, - "groupBy": Object { - "items": Object { - "type": "string", - }, - "maxItems": 3, - "minItems": 1, - "type": "array", - }, - "missingFieldsStrategy": Object { - "enum": Array [ - "doNotSuppress", - "suppress", - ], - "type": "string", - }, - }, - "required": Array [ - "groupBy", - ], - "type": "object", - }, - "dataViewId": Object { - "type": "string", - }, - "filters": Object { - "items": Object {}, - "type": "array", - }, - "index": Object { - "items": Object { - "type": "string", - }, - "type": "array", - }, - "language": Object { - "enum": Array [ - "kuery", - "lucene", - ], - "type": "string", - }, - "query": Object { - "type": "string", - }, - "responseActions": Object { - "items": Object { - "anyOf": Array [ - Object { - "additionalProperties": false, - "properties": Object { - "actionTypeId": Object { - "const": ".osquery", - "type": "string", - }, - "params": Object { - "additionalProperties": false, - "properties": Object { - "ecsMapping": Object { - "additionalProperties": Object { - "additionalProperties": false, - "properties": Object { - "field": Object { - "type": "string", - }, - "value": Object { - "anyOf": Array [ - Object { - "type": "string", - }, - Object { - "items": Object { - "type": "string", - }, - "type": "array", - }, - ], - }, - }, - "type": "object", - }, - "properties": Object {}, - "type": "object", - }, - "packId": Object { - "type": "string", - }, - "queries": Object { - "items": Object { - "additionalProperties": false, - "properties": Object { - "ecs_mapping": Object { - "$ref": "#/allOf/1/anyOf/0/properties/responseActions/items/anyOf/0/properties/params/properties/ecsMapping", - }, - "id": Object { - "type": "string", - }, - "platform": Object { - "type": "string", - }, - "query": Object { - "type": "string", - }, - "removed": Object { - "type": "boolean", - }, - "snapshot": Object { - "type": "boolean", - }, - "version": Object { - "type": "string", - }, - }, - "required": Array [ - "id", - "query", - ], - "type": "object", - }, - "type": "array", - }, - "query": Object { - "type": "string", - }, - "savedQueryId": Object { - "type": "string", - }, - "timeout": Object { - "type": "number", - }, - }, - "type": "object", - }, - }, - "required": Array [ - "actionTypeId", - "params", - ], - "type": "object", - }, - Object { - "additionalProperties": false, - "properties": Object { - "actionTypeId": Object { - "const": ".endpoint", - "type": "string", - }, - "params": Object { - "anyOf": Array [ - Object { - "additionalProperties": false, - "properties": Object { - "command": Object { - "const": "isolate", - "type": "string", - }, - "comment": Object { - "type": "string", - }, - }, - "required": Array [ - "command", - ], - "type": "object", - }, - Object { - "additionalProperties": false, - "properties": Object { - "command": Object { - "enum": Array [ - "kill-process", - "suspend-process", - ], - "type": "string", - }, - "comment": Object { - "type": "string", - }, - "config": Object { - "additionalProperties": false, - "properties": Object { - "field": Object { - "type": "string", - }, - "overwrite": Object { - "default": true, - "type": "boolean", - }, - }, - "required": Array [ - "field", - ], - "type": "object", - }, - }, - "required": Array [ - "command", - "config", - ], - "type": "object", - }, - ], - }, + "riskScore", + "riskScoreMapping", + "severity", + "severityMapping", + "threat", + "to", + "references", + "version", + "exceptionsList", + ], + "type": "object", + }, + Object { + "anyOf": Array [ + Object { + "additionalProperties": false, + "properties": Object { + "alertSuppression": Object { + "additionalProperties": false, + "properties": Object { + "duration": Object { + "additionalProperties": false, + "properties": Object { + "unit": Object { + "enum": Array [ + "s", + "m", + "h", + ], + "type": "string", + }, + "value": Object { + "minimum": 1, + "type": "integer", }, - "required": Array [ - "actionTypeId", - "params", - ], - "type": "object", }, - ], + "required": Array [ + "value", + "unit", + ], + "type": "object", + }, + "groupBy": Object { + "items": Object { + "type": "string", + }, + "maxItems": 3, + "minItems": 1, + "type": "array", + }, + "missingFieldsStrategy": Object { + "enum": Array [ + "doNotSuppress", + "suppress", + ], + "type": "string", + }, + }, + "required": Array [ + "groupBy", + ], + "type": "object", + }, + "dataViewId": Object { + "type": "string", + }, + "filters": Object { + "items": Object {}, + "type": "array", + }, + "index": Object { + "items": Object { + "type": "string", }, "type": "array", }, + "language": Object { + "enum": Array [ + "kuery", + "lucene", + ], + "type": "string", + }, + "query": Object { + "type": "string", + }, "savedId": Object { "type": "string", }, @@ -9469,12 +9801,6 @@ Object { "query": Object { "$ref": "#/allOf/1/anyOf/0/properties/query", }, - "responseActions": Object { - "items": Object { - "$ref": "#/allOf/1/anyOf/0/properties/responseActions/items", - }, - "type": "array", - }, "savedId": Object { "$ref": "#/allOf/1/anyOf/0/properties/savedId", }, @@ -9667,6 +9993,175 @@ Object { }, "type": "array", }, + "responseActions": Object { + "items": Object { + "anyOf": Array [ + Object { + "additionalProperties": false, + "properties": Object { + "actionTypeId": Object { + "const": ".osquery", + "type": "string", + }, + "params": Object { + "additionalProperties": false, + "properties": Object { + "ecsMapping": Object { + "additionalProperties": Object { + "additionalProperties": false, + "properties": Object { + "field": Object { + "type": "string", + }, + "value": Object { + "anyOf": Array [ + Object { + "type": "string", + }, + Object { + "items": Object { + "type": "string", + }, + "type": "array", + }, + ], + }, + }, + "type": "object", + }, + "properties": Object {}, + "type": "object", + }, + "packId": Object { + "type": "string", + }, + "queries": Object { + "items": Object { + "additionalProperties": false, + "properties": Object { + "ecs_mapping": Object { + "$ref": "#/allOf/0/properties/responseActions/items/anyOf/0/properties/params/properties/ecsMapping", + }, + "id": Object { + "type": "string", + }, + "platform": Object { + "type": "string", + }, + "query": Object { + "type": "string", + }, + "removed": Object { + "type": "boolean", + }, + "snapshot": Object { + "type": "boolean", + }, + "version": Object { + "type": "string", + }, + }, + "required": Array [ + "id", + "query", + ], + "type": "object", + }, + "type": "array", + }, + "query": Object { + "type": "string", + }, + "savedQueryId": Object { + "type": "string", + }, + "timeout": Object { + "type": "number", + }, + }, + "type": "object", + }, + }, + "required": Array [ + "actionTypeId", + "params", + ], + "type": "object", + }, + Object { + "additionalProperties": false, + "properties": Object { + "actionTypeId": Object { + "const": ".endpoint", + "type": "string", + }, + "params": Object { + "anyOf": Array [ + Object { + "additionalProperties": false, + "properties": Object { + "command": Object { + "const": "isolate", + "type": "string", + }, + "comment": Object { + "type": "string", + }, + }, + "required": Array [ + "command", + ], + "type": "object", + }, + Object { + "additionalProperties": false, + "properties": Object { + "command": Object { + "enum": Array [ + "kill-process", + "suspend-process", + ], + "type": "string", + }, + "comment": Object { + "type": "string", + }, + "config": Object { + "additionalProperties": false, + "properties": Object { + "field": Object { + "type": "string", + }, + "overwrite": Object { + "default": true, + "type": "boolean", + }, + }, + "required": Array [ + "field", + ], + "type": "object", + }, + }, + "required": Array [ + "command", + "config", + ], + "type": "object", + }, + ], + }, + }, + "required": Array [ + "actionTypeId", + "params", + ], + "type": "object", + }, + ], + }, + "type": "array", + }, "riskScore": Object { "maximum": 100, "minimum": 0, diff --git a/x-pack/plugins/alerting/server/lib/alerting_event_logger/alerting_event_logger.test.ts b/x-pack/plugins/alerting/server/lib/alerting_event_logger/alerting_event_logger.test.ts index 82e8663bd6bf8..082d5ea6381df 100644 --- a/x-pack/plugins/alerting/server/lib/alerting_event_logger/alerting_event_logger.test.ts +++ b/x-pack/plugins/alerting/server/lib/alerting_event_logger/alerting_event_logger.test.ts @@ -807,6 +807,15 @@ describe('AlertingEventLogger', () => { expect(eventLogger.logEvent).toHaveBeenCalledWith(event); }); + + test('should log action event with uuid', () => { + alertingEventLogger.initialize({ context: ruleContext, runDate, ruleData }); + alertingEventLogger.logAction({ ...action, uuid: 'abcdefg' }); + + const event = createActionExecuteRecord(ruleContext, ruleData, [alertSO], action); + + expect(eventLogger.logEvent).toHaveBeenCalledWith(event); + }); }); describe('done()', () => { diff --git a/x-pack/plugins/alerting/server/lib/alerting_event_logger/alerting_event_logger.ts b/x-pack/plugins/alerting/server/lib/alerting_event_logger/alerting_event_logger.ts index f29e1e00473b2..1607f6090b10c 100644 --- a/x-pack/plugins/alerting/server/lib/alerting_event_logger/alerting_event_logger.ts +++ b/x-pack/plugins/alerting/server/lib/alerting_event_logger/alerting_event_logger.ts @@ -78,6 +78,9 @@ interface AlertOpts { export interface ActionOpts { id: string; + // uuid is typed as optional but in reality it is always + // populated - https://github.com/elastic/kibana/issues/195255 + uuid?: string; typeId: string; alertId?: string; alertGroup?: string; diff --git a/x-pack/plugins/alerting/server/task_runner/action_scheduler/action_scheduler.test.ts b/x-pack/plugins/alerting/server/task_runner/action_scheduler/action_scheduler.test.ts index 600f6aedbe039..b6f250b47205e 100644 --- a/x-pack/plugins/alerting/server/task_runner/action_scheduler/action_scheduler.test.ts +++ b/x-pack/plugins/alerting/server/task_runner/action_scheduler/action_scheduler.test.ts @@ -60,7 +60,9 @@ const defaultSchedulerContext = getDefaultSchedulerContext( const defaultExecutionResponse = { errors: false, - items: [{ actionTypeId: 'test', id: '1', response: ExecutionResponseType.SUCCESS }], + items: [ + { actionTypeId: 'test', id: '1', uuid: '111-111', response: ExecutionResponseType.SUCCESS }, + ], }; let ruleRunMetricsStore: RuleRunMetricsStore; @@ -99,7 +101,7 @@ describe('Action Scheduler', () => { }); afterAll(() => clock.restore()); - test('enqueues execution per selected action', async () => { + test('schedules execution per selected action', async () => { const alerts = generateAlert({ id: 1 }); const actionScheduler = new ActionScheduler(getSchedulerContext()); await actionScheduler.run(alerts); @@ -138,6 +140,7 @@ describe('Action Scheduler', () => { "type": "SAVED_OBJECT", }, "spaceId": "test1", + "uuid": "111-111", }, ], ] @@ -146,6 +149,7 @@ describe('Action Scheduler', () => { expect(alertingEventLogger.logAction).toHaveBeenCalledTimes(1); expect(alertingEventLogger.logAction).toHaveBeenNthCalledWith(1, { id: '1', + uuid: '111-111', typeId: 'test', alertId: '1', alertGroup: 'default', @@ -368,6 +372,7 @@ describe('Action Scheduler', () => { "type": "SAVED_OBJECT", }, "spaceId": "test1", + "uuid": "111-111", }, ], ] @@ -409,6 +414,7 @@ describe('Action Scheduler', () => { "type": "SAVED_OBJECT", }, "spaceId": "test1", + "uuid": "111-111", }, ], ] @@ -437,11 +443,13 @@ describe('Action Scheduler', () => { { actionTypeId: 'test2', id: '1', + uuid: '111-111', response: ExecutionResponseType.SUCCESS, }, { actionTypeId: 'test2', id: '2', + uuid: '222-222', response: ExecutionResponseType.SUCCESS, }, ], @@ -508,20 +516,23 @@ describe('Action Scheduler', () => { actionsClient.bulkEnqueueExecution.mockResolvedValueOnce({ errors: false, items: [ - { actionTypeId: 'test', id: '1', response: ExecutionResponseType.SUCCESS }, + { actionTypeId: 'test', id: '1', uuid: '222-222', response: ExecutionResponseType.SUCCESS }, { actionTypeId: 'test-action-type-id', id: '2', + uuid: '222-222', response: ExecutionResponseType.SUCCESS, }, { actionTypeId: 'another-action-type-id', id: '4', + uuid: '444-444', response: ExecutionResponseType.SUCCESS, }, { actionTypeId: 'another-action-type-id', id: '5', + uuid: '555-555', response: ExecutionResponseType.SUCCESS, }, ], @@ -537,6 +548,7 @@ describe('Action Scheduler', () => { contextVal: 'My other {{context.value}} goes here', stateVal: 'My other {{state.value}} goes here', }, + uuid: '222-222', }, { id: '3', @@ -547,6 +559,7 @@ describe('Action Scheduler', () => { contextVal: '{{context.value}} goes here', stateVal: '{{state.value}} goes here', }, + uuid: '333-333', }, { id: '4', @@ -557,6 +570,7 @@ describe('Action Scheduler', () => { contextVal: '{{context.value}} goes here', stateVal: '{{state.value}} goes here', }, + uuid: '444-444', }, { id: '5', @@ -567,6 +581,7 @@ describe('Action Scheduler', () => { contextVal: '{{context.value}} goes here', stateVal: '{{state.value}} goes here', }, + uuid: '555-555', }, ]; const actionScheduler = new ActionScheduler( @@ -612,16 +627,19 @@ describe('Action Scheduler', () => { { actionTypeId: 'test', id: '1', + uuid: '111-111', response: ExecutionResponseType.SUCCESS, }, { actionTypeId: 'test', id: '2', + uuid: '222-222', response: ExecutionResponseType.SUCCESS, }, { actionTypeId: 'test', id: '3', + uuid: '333-333', response: ExecutionResponseType.QUEUED_ACTIONS_LIMIT_ERROR, }, ], @@ -636,6 +654,7 @@ describe('Action Scheduler', () => { contextVal: 'My other {{context.value}} goes here', stateVal: 'My other {{state.value}} goes here', }, + uuid: '111-111', }, { id: '2', @@ -646,6 +665,7 @@ describe('Action Scheduler', () => { contextVal: 'My other {{context.value}} goes here', stateVal: 'My other {{state.value}} goes here', }, + uuid: '222-222', }, { id: '3', @@ -656,6 +676,7 @@ describe('Action Scheduler', () => { contextVal: '{{context.value}} goes here', stateVal: '{{state.value}} goes here', }, + uuid: '333-333', }, ]; const actionScheduler = new ActionScheduler( @@ -679,7 +700,7 @@ describe('Action Scheduler', () => { test('schedules alerts with recovered actions', async () => { const actions = [ { - id: '1', + id: 'action-2', group: 'recovered', actionTypeId: 'test', params: { @@ -689,6 +710,7 @@ describe('Action Scheduler', () => { alertVal: 'My {{rule.id}} {{rule.name}} {{rule.spaceId}} {{rule.tags}} {{alert.id}} goes here', }, + uuid: '222-222', }, ]; const actionScheduler = new ActionScheduler( @@ -711,7 +733,7 @@ describe('Action Scheduler', () => { "apiKey": "MTIzOmFiYw==", "consumer": "rule-consumer", "executionId": "5f6aa57d-3e22-484e-bae8-cbed868f4d28", - "id": "1", + "id": "action-2", "params": Object { "alertVal": "My 1 name-of-alert test1 tag-A,tag-B 1 goes here", "contextVal": "My goes here", @@ -734,6 +756,7 @@ describe('Action Scheduler', () => { "type": "SAVED_OBJECT", }, "spaceId": "test1", + "uuid": "222-222", }, ], ] @@ -883,6 +906,7 @@ describe('Action Scheduler', () => { { actionTypeId: 'testActionTypeId', id: '1', + uuid: '111-111', response: ExecutionResponseType.SUCCESS, }, ], @@ -914,6 +938,7 @@ describe('Action Scheduler', () => { message: 'New: {{alerts.new.count}} Ongoing: {{alerts.ongoing.count}} Recovered: {{alerts.recovered.count}}', }, + uuid: '111-111', }, ], }, @@ -957,6 +982,7 @@ describe('Action Scheduler', () => { "type": "SAVED_OBJECT", }, "spaceId": "test1", + "uuid": "111-111", }, ], ] @@ -964,6 +990,7 @@ describe('Action Scheduler', () => { expect(alertingEventLogger.logAction).toBeCalledWith({ alertSummary: { new: 1, ongoing: 0, recovered: 0 }, id: '1', + uuid: '111-111', typeId: 'testActionTypeId', }); }); @@ -1012,6 +1039,7 @@ describe('Action Scheduler', () => { { actionTypeId: 'testActionTypeId', id: '1', + uuid: '111-111', response: ExecutionResponseType.SUCCESS, }, ], @@ -1095,6 +1123,7 @@ describe('Action Scheduler', () => { "type": "SAVED_OBJECT", }, "spaceId": "test1", + "uuid": "111-111", }, ], ] @@ -1102,6 +1131,7 @@ describe('Action Scheduler', () => { expect(alertingEventLogger.logAction).toBeCalledWith({ alertSummary: { new: 1, ongoing: 0, recovered: 0 }, id: '1', + uuid: '111-111', typeId: 'testActionTypeId', }); }); @@ -1256,10 +1286,11 @@ describe('Action Scheduler', () => { actionsClient.bulkEnqueueExecution.mockResolvedValueOnce({ errors: false, items: [ - { actionTypeId: 'test', id: '1', response: ExecutionResponseType.SUCCESS }, + { actionTypeId: 'test', id: '1', uuid: '111-111', response: ExecutionResponseType.SUCCESS }, { actionTypeId: 'test', id: '2', + uuid: '222-222', response: ExecutionResponseType.SUCCESS, }, ], @@ -1276,6 +1307,7 @@ describe('Action Scheduler', () => { alertVal: 'My {{rule.id}} {{rule.name}} {{rule.spaceId}} {{rule.tags}} {{alert.id}} goes here', }, + uuid: '111-111', }, { id: '2', @@ -1288,6 +1320,7 @@ describe('Action Scheduler', () => { alertVal: 'My {{rule.id}} {{rule.name}} {{rule.spaceId}} {{rule.tags}} {{alert.id}} goes here', }, + uuid: '222-222', }, ]; const actionScheduler = new ActionScheduler( @@ -1333,6 +1366,7 @@ describe('Action Scheduler', () => { "type": "SAVED_OBJECT", }, "spaceId": "test1", + "uuid": "111-111", }, Object { "actionTypeId": "test", @@ -1362,6 +1396,7 @@ describe('Action Scheduler', () => { "type": "SAVED_OBJECT", }, "spaceId": "test1", + "uuid": "222-222", }, ], ] @@ -1448,6 +1483,7 @@ describe('Action Scheduler', () => { { actionTypeId: 'testActionTypeId', id: '1', + uuid: '111-111', response: ExecutionResponseType.SUCCESS, }, ], @@ -1518,6 +1554,7 @@ describe('Action Scheduler', () => { { actionTypeId: 'testActionTypeId', id: '1', + uuid: '111-111', response: ExecutionResponseType.SUCCESS, }, ], @@ -1541,7 +1578,7 @@ describe('Action Scheduler', () => { actions: [ { id: '1', - uuid: '111', + uuid: '111-111', group: 'default', actionTypeId: 'testActionTypeId', frequency: { @@ -1587,17 +1624,19 @@ describe('Action Scheduler', () => { ], source: { source: { id: '1', type: RULE_SAVED_OBJECT_TYPE }, type: 'SAVED_OBJECT' }, spaceId: 'test1', + uuid: '111-111', }, ]); expect(alertingEventLogger.logAction).toHaveBeenCalledWith({ alertGroup: 'default', alertId: '1', id: '1', + uuid: '111-111', typeId: 'testActionTypeId', }); expect(defaultSchedulerContext.logger.debug).toHaveBeenCalledTimes(1); expect(defaultSchedulerContext.logger.debug).toHaveBeenCalledWith( - '(2) alerts have been filtered out for: testActionTypeId:111' + '(2) alerts have been filtered out for: testActionTypeId:111-111' ); }); @@ -1840,6 +1879,7 @@ describe('Action Scheduler', () => { "type": "SAVED_OBJECT", }, "spaceId": "test1", + "uuid": "111-111", }, Object { "actionTypeId": "test", @@ -1869,6 +1909,7 @@ describe('Action Scheduler', () => { "type": "SAVED_OBJECT", }, "spaceId": "test1", + "uuid": "111-111", }, Object { "actionTypeId": "test", @@ -1898,6 +1939,7 @@ describe('Action Scheduler', () => { "type": "SAVED_OBJECT", }, "spaceId": "test1", + "uuid": "111-111", }, ], ] @@ -2261,12 +2303,13 @@ describe('Action Scheduler', () => { const executorParams = getSchedulerContext({ rule: { ...defaultSchedulerContext.rule, + actions: [], systemActions: [ { id: '1', actionTypeId: '.test-system-action', params: actionsParams, - uui: 'test', + uuid: 'test', }, ], }, @@ -2360,6 +2403,7 @@ describe('Action Scheduler', () => { "type": "SAVED_OBJECT", }, "spaceId": "test1", + "uuid": "test", }, ], ] @@ -2368,6 +2412,7 @@ describe('Action Scheduler', () => { expect(alertingEventLogger.logAction).toBeCalledWith({ alertSummary: { new: 1, ongoing: 0, recovered: 0 }, id: '1', + uuid: 'test', typeId: '.test-system-action', }); }); @@ -2387,6 +2432,7 @@ describe('Action Scheduler', () => { const executorParams = getSchedulerContext({ rule: { ...defaultSchedulerContext.rule, + actions: [], systemActions: [ { id: 'action-id', @@ -2443,6 +2489,7 @@ describe('Action Scheduler', () => { }, rule: { ...defaultSchedulerContext.rule, + actions: [], systemActions: [ { id: 'action-id', @@ -2477,6 +2524,7 @@ describe('Action Scheduler', () => { const executorParams = getSchedulerContext({ rule: { ...defaultSchedulerContext.rule, + actions: [], systemActions: [ { id: '1', diff --git a/x-pack/plugins/alerting/server/task_runner/action_scheduler/action_scheduler.ts b/x-pack/plugins/alerting/server/task_runner/action_scheduler/action_scheduler.ts index 3b804ce3da413..44822657ba86f 100644 --- a/x-pack/plugins/alerting/server/task_runner/action_scheduler/action_scheduler.ts +++ b/x-pack/plugins/alerting/server/task_runner/action_scheduler/action_scheduler.ts @@ -5,8 +5,6 @@ * 2.0. */ -import { getRuleDetailsRoute, triggersActionsRoute } from '@kbn/rule-data-utils'; -import { asSavedObjectExecutionSource } from '@kbn/actions-plugin/server'; import { createTaskRunError, isEphemeralTaskRejectedDueToCapacityError, @@ -19,77 +17,21 @@ import { } from '@kbn/actions-plugin/server/create_execute_function'; import { ActionsCompletion } from '@kbn/alerting-state-types'; import { chunk } from 'lodash'; -import { CombinedSummarizedAlerts, ThrottledActions } from '../../types'; -import { injectActionParams } from '../inject_action_params'; -import { ActionSchedulerOptions, IActionScheduler, RuleUrl } from './types'; -import { - transformActionParams, - TransformActionParamsOptions, - transformSummaryActionParams, -} from '../transform_action_params'; +import { ThrottledActions } from '../../types'; +import { ActionSchedulerOptions, ActionsToSchedule, IActionScheduler } from './types'; import { Alert } from '../../alert'; import { AlertInstanceContext, AlertInstanceState, - RuleAction, RuleTypeParams, RuleTypeState, - SanitizedRule, RuleAlertData, - RuleSystemAction, } from '../../../common'; -import { - generateActionHash, - getSummaryActionsFromTaskState, - getSummaryActionTimeBounds, - isActionOnInterval, -} from './rule_action_helper'; -import { RULE_SAVED_OBJECT_TYPE } from '../../saved_objects'; -import { ConnectorAdapter } from '../../connector_adapters/types'; +import { getSummaryActionsFromTaskState } from './lib'; import { withAlertingSpan } from '../lib'; import * as schedulers from './schedulers'; -interface LogAction { - id: string; - typeId: string; - alertId?: string; - alertGroup?: string; - alertSummary?: { - new: number; - ongoing: number; - recovered: number; - }; -} - -interface RunSummarizedActionArgs { - action: RuleAction; - summarizedAlerts: CombinedSummarizedAlerts; - spaceId: string; - bulkActions: EnqueueExecutionOptions[]; -} - -interface RunSystemActionArgs { - action: RuleSystemAction; - connectorAdapter: ConnectorAdapter; - summarizedAlerts: CombinedSummarizedAlerts; - rule: SanitizedRule; - ruleProducer: string; - spaceId: string; - bulkActions: EnqueueExecutionOptions[]; -} - -interface RunActionArgs< - State extends AlertInstanceState, - Context extends AlertInstanceContext, - ActionGroupIds extends string, - RecoveryActionGroupId extends string -> { - action: RuleAction; - alert: Alert; - ruleId: string; - spaceId: string; - bulkActions: EnqueueExecutionOptions[]; -} +const BULK_SCHEDULE_CHUNK_SIZE = 1000; export interface RunResult { throttledSummaryActions: ThrottledActions; @@ -110,9 +52,6 @@ export class ActionScheduler< > = []; private ephemeralActionsToSchedule: number; - private CHUNK_SIZE = 1000; - private ruleTypeActionGroups?: Map; - private previousStartedAt: Date | null; constructor( private readonly context: ActionSchedulerOptions< @@ -127,11 +66,6 @@ export class ActionScheduler< > ) { this.ephemeralActionsToSchedule = context.taskRunnerContext.maxEphemeralActionsPerRule; - this.ruleTypeActionGroups = new Map( - context.ruleType.actionGroups.map((actionGroup) => [actionGroup.id, actionGroup.name]) - ); - this.previousStartedAt = context.previousStartedAt; - for (const [_, scheduler] of Object.entries(schedulers)) { this.schedulers.push(new scheduler(context)); } @@ -148,148 +82,30 @@ export class ActionScheduler< summaryActions: this.context.taskInstance.state?.summaryActions, }); - const executables = []; + const allActionsToScheduleResult: ActionsToSchedule[] = []; for (const scheduler of this.schedulers) { - executables.push( - ...(await scheduler.generateExecutables({ alerts, throttledSummaryActions })) + allActionsToScheduleResult.push( + ...(await scheduler.getActionsToSchedule({ alerts, throttledSummaryActions })) ); } - if (executables.length === 0) { + if (allActionsToScheduleResult.length === 0) { return { throttledSummaryActions }; } - const { - CHUNK_SIZE, - context: { - logger, - alertingEventLogger, - ruleRunMetricsStore, - taskRunnerContext: { actionsConfigMap }, - taskInstance: { - params: { spaceId, alertId: ruleId }, - }, - }, - } = this; - - const logActions: Record = {}; - const bulkActions: EnqueueExecutionOptions[] = []; - let bulkActionsResponse: ExecutionResponseItem[] = []; + const bulkScheduleRequest: EnqueueExecutionOptions[] = []; - this.context.ruleRunMetricsStore.incrementNumberOfGeneratedActions(executables.length); - - for (const { action, alert, summarizedAlerts } of executables) { - const { actionTypeId } = action; - - ruleRunMetricsStore.incrementNumberOfGeneratedActionsByConnectorType(actionTypeId); - if (ruleRunMetricsStore.hasReachedTheExecutableActionsLimit(actionsConfigMap)) { - ruleRunMetricsStore.setTriggeredActionsStatusByConnectorType({ - actionTypeId, - status: ActionsCompletion.PARTIAL, - }); - logger.debug( - `Rule "${this.context.rule.id}" skipped scheduling action "${action.id}" because the maximum number of allowed actions has been reached.` - ); - break; - } - - if ( - ruleRunMetricsStore.hasReachedTheExecutableActionsLimitByConnectorType({ - actionTypeId, - actionsConfigMap, - }) - ) { - if (!ruleRunMetricsStore.hasConnectorTypeReachedTheLimit(actionTypeId)) { - logger.debug( - `Rule "${this.context.rule.id}" skipped scheduling action "${action.id}" because the maximum number of allowed actions for connector type ${actionTypeId} has been reached.` - ); - } - ruleRunMetricsStore.setTriggeredActionsStatusByConnectorType({ - actionTypeId, - status: ActionsCompletion.PARTIAL, - }); - continue; - } - - if (!this.isExecutableAction(action)) { - this.context.logger.warn( - `Rule "${this.context.taskInstance.params.alertId}" skipped scheduling action "${action.id}" because it is disabled` - ); - continue; - } - - ruleRunMetricsStore.incrementNumberOfTriggeredActions(); - ruleRunMetricsStore.incrementNumberOfTriggeredActionsByConnectorType(actionTypeId); - - if (!this.isSystemAction(action) && summarizedAlerts) { - const defaultAction = action as RuleAction; - if (isActionOnInterval(action)) { - throttledSummaryActions[defaultAction.uuid!] = { date: new Date().toISOString() }; - } - - logActions[defaultAction.id] = await this.runSummarizedAction({ - action, - summarizedAlerts, - spaceId, - bulkActions, - }); - } else if (summarizedAlerts && this.isSystemAction(action)) { - const hasConnectorAdapter = this.context.taskRunnerContext.connectorAdapterRegistry.has( - action.actionTypeId - ); - /** - * System actions without an adapter - * cannot be executed - * - */ - if (!hasConnectorAdapter) { - this.context.logger.warn( - `Rule "${this.context.taskInstance.params.alertId}" skipped scheduling system action "${action.id}" because no connector adapter is configured` - ); - - continue; - } - - const connectorAdapter = this.context.taskRunnerContext.connectorAdapterRegistry.get( - action.actionTypeId - ); - logActions[action.id] = await this.runSystemAction({ - action, - connectorAdapter, - summarizedAlerts, - rule: this.context.rule, - ruleProducer: this.context.ruleType.producer, - spaceId, - bulkActions, - }); - } else if (!this.isSystemAction(action) && alert) { - const defaultAction = action as RuleAction; - logActions[defaultAction.id] = await this.runAction({ - action, - spaceId, - alert, - ruleId, - bulkActions, - }); - - const actionGroup = defaultAction.group; - if (!this.isRecoveredAlert(actionGroup)) { - if (isActionOnInterval(action)) { - alert.updateLastScheduledActions( - defaultAction.group as ActionGroupIds, - generateActionHash(action), - defaultAction.uuid - ); - } else { - alert.updateLastScheduledActions(defaultAction.group as ActionGroupIds); - } - alert.unscheduleActions(); - } - } + for (const result of allActionsToScheduleResult) { + await this.runActionAsEphemeralOrAddToBulkScheduleRequest({ + enqueueOptions: result.actionToEnqueue, + bulkScheduleRequest, + }); } - if (!!bulkActions.length) { - for (const c of chunk(bulkActions, CHUNK_SIZE)) { + let bulkScheduleResponse: ExecutionResponseItem[] = []; + + if (!!bulkScheduleRequest.length) { + for (const c of chunk(bulkScheduleRequest, BULK_SCHEDULE_CHUNK_SIZE)) { let enqueueResponse; try { enqueueResponse = await withAlertingSpan('alerting:bulk-enqueue-actions', () => @@ -302,7 +118,7 @@ export class ActionScheduler< throw createTaskRunError(e, TaskErrorSource.FRAMEWORK); } if (enqueueResponse.errors) { - bulkActionsResponse = bulkActionsResponse.concat( + bulkScheduleResponse = bulkScheduleResponse.concat( enqueueResponse.items.filter( (i) => i.response === ExecutionResponseType.QUEUED_ACTIONS_LIMIT_ERROR ) @@ -311,280 +127,53 @@ export class ActionScheduler< } } - if (!!bulkActionsResponse.length) { - for (const r of bulkActionsResponse) { + const actionsToNotLog: string[] = []; + if (!!bulkScheduleResponse.length) { + for (const r of bulkScheduleResponse) { if (r.response === ExecutionResponseType.QUEUED_ACTIONS_LIMIT_ERROR) { - ruleRunMetricsStore.setHasReachedQueuedActionsLimit(true); - ruleRunMetricsStore.decrementNumberOfTriggeredActions(); - ruleRunMetricsStore.decrementNumberOfTriggeredActionsByConnectorType(r.actionTypeId); - ruleRunMetricsStore.setTriggeredActionsStatusByConnectorType({ + this.context.ruleRunMetricsStore.setHasReachedQueuedActionsLimit(true); + this.context.ruleRunMetricsStore.decrementNumberOfTriggeredActions(); + this.context.ruleRunMetricsStore.decrementNumberOfTriggeredActionsByConnectorType( + r.actionTypeId + ); + this.context.ruleRunMetricsStore.setTriggeredActionsStatusByConnectorType({ actionTypeId: r.actionTypeId, status: ActionsCompletion.PARTIAL, }); - logger.debug( + this.context.logger.debug( `Rule "${this.context.rule.id}" skipped scheduling action "${r.id}" because the maximum number of queued actions has been reached.` ); - delete logActions[r.id]; + const uuid = r.uuid; + // uuid is typed as optional but in reality it is always + // populated - https://github.com/elastic/kibana/issues/195255 + if (uuid) { + actionsToNotLog.push(uuid); + } } } } - const logActionsValues = Object.values(logActions); - if (!!logActionsValues.length) { - for (const action of logActionsValues) { - alertingEventLogger.logAction(action); - } - } - - return { throttledSummaryActions }; - } - - private async runSummarizedAction({ - action, - summarizedAlerts, - spaceId, - bulkActions, - }: RunSummarizedActionArgs): Promise { - const { start, end } = getSummaryActionTimeBounds( - action, - this.context.rule.schedule, - this.previousStartedAt + const actionsToLog = allActionsToScheduleResult.filter( + (result) => result.actionToLog.uuid && !actionsToNotLog.includes(result.actionToLog.uuid) ); - const ruleUrl = this.buildRuleUrl(spaceId, start, end); - const actionToRun = { - ...action, - params: injectActionParams({ - actionTypeId: action.actionTypeId, - ruleUrl, - ruleName: this.context.rule.name, - actionParams: transformSummaryActionParams({ - alerts: summarizedAlerts, - rule: this.context.rule, - ruleTypeId: this.context.ruleType.id, - actionId: action.id, - actionParams: action.params, - spaceId, - actionsPlugin: this.context.taskRunnerContext.actionsPlugin, - actionTypeId: action.actionTypeId, - kibanaBaseUrl: this.context.taskRunnerContext.kibanaBaseUrl, - ruleUrl: ruleUrl?.absoluteUrl, - }), - }), - }; - await this.actionRunOrAddToBulk({ - enqueueOptions: this.getEnqueueOptions(actionToRun), - bulkActions, - }); - - return { - id: action.id, - typeId: action.actionTypeId, - alertSummary: { - new: summarizedAlerts.new.count, - ongoing: summarizedAlerts.ongoing.count, - recovered: summarizedAlerts.recovered.count, - }, - }; - } - - private async runSystemAction({ - action, - spaceId, - connectorAdapter, - summarizedAlerts, - rule, - ruleProducer, - bulkActions, - }: RunSystemActionArgs): Promise { - const ruleUrl = this.buildRuleUrl(spaceId); - - const connectorAdapterActionParams = connectorAdapter.buildActionParams({ - alerts: summarizedAlerts, - rule: { - id: rule.id, - tags: rule.tags, - name: rule.name, - consumer: rule.consumer, - producer: ruleProducer, - }, - ruleUrl: ruleUrl?.absoluteUrl, - spaceId, - params: action.params, - }); - - const actionToRun = Object.assign(action, { params: connectorAdapterActionParams }); - - await this.actionRunOrAddToBulk({ - enqueueOptions: this.getEnqueueOptions(actionToRun), - bulkActions, - }); - - return { - id: action.id, - typeId: action.actionTypeId, - alertSummary: { - new: summarizedAlerts.new.count, - ongoing: summarizedAlerts.ongoing.count, - recovered: summarizedAlerts.recovered.count, - }, - }; - } - - private async runAction({ - action, - spaceId, - alert, - ruleId, - bulkActions, - }: RunActionArgs): Promise { - const ruleUrl = this.buildRuleUrl(spaceId); - const executableAlert = alert!; - const actionGroup = action.group as ActionGroupIds; - const transformActionParamsOptions: TransformActionParamsOptions = { - actionsPlugin: this.context.taskRunnerContext.actionsPlugin, - alertId: ruleId, - alertType: this.context.ruleType.id, - actionTypeId: action.actionTypeId, - alertName: this.context.rule.name, - spaceId, - tags: this.context.rule.tags, - alertInstanceId: executableAlert.getId(), - alertUuid: executableAlert.getUuid(), - alertActionGroup: actionGroup, - alertActionGroupName: this.ruleTypeActionGroups!.get(actionGroup)!, - context: executableAlert.getContext(), - actionId: action.id, - state: executableAlert.getState(), - kibanaBaseUrl: this.context.taskRunnerContext.kibanaBaseUrl, - alertParams: this.context.rule.params, - actionParams: action.params, - flapping: executableAlert.getFlapping(), - ruleUrl: ruleUrl?.absoluteUrl, - }; - - if (executableAlert.isAlertAsData()) { - transformActionParamsOptions.aadAlert = executableAlert.getAlertAsData(); - } - const actionToRun = { - ...action, - params: injectActionParams({ - actionTypeId: action.actionTypeId, - ruleUrl, - ruleName: this.context.rule.name, - actionParams: transformActionParams(transformActionParamsOptions), - }), - }; - - await this.actionRunOrAddToBulk({ - enqueueOptions: this.getEnqueueOptions(actionToRun), - bulkActions, - }); - - return { - id: action.id, - typeId: action.actionTypeId, - alertId: alert.getId(), - alertGroup: action.group, - }; - } - - private isExecutableAction(action: RuleAction | RuleSystemAction) { - return this.context.taskRunnerContext.actionsPlugin.isActionExecutable( - action.id, - action.actionTypeId, - { - notifyUsage: true, + if (!!actionsToLog.length) { + for (const action of actionsToLog) { + this.context.alertingEventLogger.logAction(action.actionToLog); } - ); - } - - private isSystemAction(action?: RuleAction | RuleSystemAction): action is RuleSystemAction { - return this.context.taskRunnerContext.actionsPlugin.isSystemActionConnector(action?.id ?? ''); - } - - private isRecoveredAlert(actionGroup: string) { - return actionGroup === this.context.ruleType.recoveryActionGroup.id; - } - - private buildRuleUrl(spaceId: string, start?: number, end?: number): RuleUrl | undefined { - if (!this.context.taskRunnerContext.kibanaBaseUrl) { - return; } - const relativePath = this.context.ruleType.getViewInAppRelativeUrl - ? this.context.ruleType.getViewInAppRelativeUrl({ rule: this.context.rule, start, end }) - : `${triggersActionsRoute}${getRuleDetailsRoute(this.context.rule.id)}`; - - try { - const basePathname = new URL(this.context.taskRunnerContext.kibanaBaseUrl).pathname; - const basePathnamePrefix = basePathname !== '/' ? `${basePathname}` : ''; - const spaceIdSegment = spaceId !== 'default' ? `/s/${spaceId}` : ''; - - const ruleUrl = new URL( - [basePathnamePrefix, spaceIdSegment, relativePath].join(''), - this.context.taskRunnerContext.kibanaBaseUrl - ); - - return { - absoluteUrl: ruleUrl.toString(), - kibanaBaseUrl: this.context.taskRunnerContext.kibanaBaseUrl, - basePathname: basePathnamePrefix, - spaceIdSegment, - relativePath, - }; - } catch (error) { - this.context.logger.debug( - `Rule "${this.context.rule.id}" encountered an error while constructing the rule.url variable: ${error.message}` - ); - return; - } - } - - private getEnqueueOptions(action: RuleAction | RuleSystemAction): EnqueueExecutionOptions { - const { - context: { - apiKey, - ruleConsumer, - executionId, - taskInstance: { - params: { spaceId, alertId: ruleId }, - }, - }, - } = this; - - const namespace = spaceId === 'default' ? {} : { namespace: spaceId }; - return { - id: action.id, - params: action.params, - spaceId, - apiKey: apiKey ?? null, - consumer: ruleConsumer, - source: asSavedObjectExecutionSource({ - id: ruleId, - type: RULE_SAVED_OBJECT_TYPE, - }), - executionId, - relatedSavedObjects: [ - { - id: ruleId, - type: RULE_SAVED_OBJECT_TYPE, - namespace: namespace.namespace, - typeId: this.context.ruleType.id, - }, - ], - actionTypeId: action.actionTypeId, - }; + return { throttledSummaryActions }; } - private async actionRunOrAddToBulk({ + private async runActionAsEphemeralOrAddToBulkScheduleRequest({ enqueueOptions, - bulkActions, + bulkScheduleRequest, }: { enqueueOptions: EnqueueExecutionOptions; - bulkActions: EnqueueExecutionOptions[]; + bulkScheduleRequest: EnqueueExecutionOptions[]; }) { if ( this.context.taskRunnerContext.supportsEphemeralTasks && @@ -595,11 +184,11 @@ export class ActionScheduler< await this.context.actionsClient!.ephemeralEnqueuedExecution(enqueueOptions); } catch (err) { if (isEphemeralTaskRejectedDueToCapacityError(err)) { - bulkActions.push(enqueueOptions); + bulkScheduleRequest.push(enqueueOptions); } } } else { - bulkActions.push(enqueueOptions); + bulkScheduleRequest.push(enqueueOptions); } } } diff --git a/x-pack/plugins/alerting/server/task_runner/action_scheduler/lib/build_rule_url.test.ts b/x-pack/plugins/alerting/server/task_runner/action_scheduler/lib/build_rule_url.test.ts new file mode 100644 index 0000000000000..cb1f3c60fd992 --- /dev/null +++ b/x-pack/plugins/alerting/server/task_runner/action_scheduler/lib/build_rule_url.test.ts @@ -0,0 +1,141 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { loggingSystemMock } from '@kbn/core-logging-server-mocks'; +import { buildRuleUrl } from './build_rule_url'; +import { getRule } from '../test_fixtures'; + +const logger = loggingSystemMock.create().get(); +const rule = getRule(); + +describe('buildRuleUrl', () => { + beforeEach(() => { + jest.clearAllMocks(); + }); + + test('should return undefined if kibanaBaseUrl is not provided', () => { + expect( + buildRuleUrl({ + kibanaBaseUrl: undefined, + logger, + rule, + spaceId: 'default', + }) + ).toBeUndefined(); + }); + + test('should return the expected URL', () => { + expect( + buildRuleUrl({ + kibanaBaseUrl: 'http://localhost:5601', + logger, + rule, + spaceId: 'default', + }) + ).toEqual({ + absoluteUrl: + 'http://localhost:5601/app/management/insightsAndAlerting/triggersActions/rule/1', + basePathname: '', + kibanaBaseUrl: 'http://localhost:5601', + relativePath: '/app/management/insightsAndAlerting/triggersActions/rule/1', + spaceIdSegment: '', + }); + }); + + test('should return the expected URL for custom space', () => { + expect( + buildRuleUrl({ + kibanaBaseUrl: 'http://localhost:5601', + logger, + rule, + spaceId: 'my-special-space', + }) + ).toEqual({ + absoluteUrl: + 'http://localhost:5601/s/my-special-space/app/management/insightsAndAlerting/triggersActions/rule/1', + basePathname: '', + kibanaBaseUrl: 'http://localhost:5601', + relativePath: '/app/management/insightsAndAlerting/triggersActions/rule/1', + spaceIdSegment: '/s/my-special-space', + }); + }); + + test('should return the expected URL when getViewInAppRelativeUrl is defined', () => { + expect( + buildRuleUrl({ + getViewInAppRelativeUrl: ({ rule: r }) => `/app/test/my-custom-rule-page/${r.id}`, + kibanaBaseUrl: 'http://localhost:5601', + logger, + rule, + spaceId: 'default', + }) + ).toEqual({ + absoluteUrl: 'http://localhost:5601/app/test/my-custom-rule-page/1', + basePathname: '', + kibanaBaseUrl: 'http://localhost:5601', + relativePath: '/app/test/my-custom-rule-page/1', + spaceIdSegment: '', + }); + }); + + test('should return the expected URL when start, end and getViewInAppRelativeUrl is defined', () => { + expect( + buildRuleUrl({ + end: 987654321, + getViewInAppRelativeUrl: ({ rule: r, start: s, end: e }) => + `/app/test/my-custom-rule-page/${r.id}?start=${s}&end=${e}`, + kibanaBaseUrl: 'http://localhost:5601', + logger, + rule, + start: 123456789, + spaceId: 'default', + }) + ).toEqual({ + absoluteUrl: + 'http://localhost:5601/app/test/my-custom-rule-page/1?start=123456789&end=987654321', + basePathname: '', + kibanaBaseUrl: 'http://localhost:5601', + relativePath: '/app/test/my-custom-rule-page/1?start=123456789&end=987654321', + spaceIdSegment: '', + }); + }); + + test('should return the expected URL when start and end are defined but getViewInAppRelativeUrl is undefined', () => { + expect( + buildRuleUrl({ + end: 987654321, + kibanaBaseUrl: 'http://localhost:5601', + logger, + rule, + start: 123456789, + spaceId: 'default', + }) + ).toEqual({ + absoluteUrl: + 'http://localhost:5601/app/management/insightsAndAlerting/triggersActions/rule/1', + basePathname: '', + kibanaBaseUrl: 'http://localhost:5601', + relativePath: '/app/management/insightsAndAlerting/triggersActions/rule/1', + spaceIdSegment: '', + }); + }); + + test('should return undefined if base url is invalid', () => { + expect( + buildRuleUrl({ + kibanaBaseUrl: 'foo-url', + logger, + rule, + spaceId: 'default', + }) + ).toBeUndefined(); + + expect(logger.debug).toHaveBeenCalledWith( + `Rule "1" encountered an error while constructing the rule.url variable: Invalid URL: foo-url` + ); + }); +}); diff --git a/x-pack/plugins/alerting/server/task_runner/action_scheduler/lib/build_rule_url.ts b/x-pack/plugins/alerting/server/task_runner/action_scheduler/lib/build_rule_url.ts new file mode 100644 index 0000000000000..3df27a512c7f9 --- /dev/null +++ b/x-pack/plugins/alerting/server/task_runner/action_scheduler/lib/build_rule_url.ts @@ -0,0 +1,65 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { Logger } from '@kbn/logging'; +import { RuleTypeParams, SanitizedRule } from '@kbn/alerting-types'; +import { getRuleDetailsRoute, triggersActionsRoute } from '@kbn/rule-data-utils'; +import { GetViewInAppRelativeUrlFn } from '../../../types'; + +interface BuildRuleUrlOpts { + end?: number; + getViewInAppRelativeUrl?: GetViewInAppRelativeUrlFn; + kibanaBaseUrl: string | undefined; + logger: Logger; + rule: SanitizedRule; + spaceId: string; + start?: number; +} + +interface BuildRuleUrlResult { + absoluteUrl: string; + basePathname: string; + kibanaBaseUrl: string; + relativePath: string; + spaceIdSegment: string; +} + +export const buildRuleUrl = ( + opts: BuildRuleUrlOpts +): BuildRuleUrlResult | undefined => { + if (!opts.kibanaBaseUrl) { + return; + } + + const relativePath = opts.getViewInAppRelativeUrl + ? opts.getViewInAppRelativeUrl({ rule: opts.rule, start: opts.start, end: opts.end }) + : `${triggersActionsRoute}${getRuleDetailsRoute(opts.rule.id)}`; + + try { + const basePathname = new URL(opts.kibanaBaseUrl).pathname; + const basePathnamePrefix = basePathname !== '/' ? `${basePathname}` : ''; + const spaceIdSegment = opts.spaceId !== 'default' ? `/s/${opts.spaceId}` : ''; + + const ruleUrl = new URL( + [basePathnamePrefix, spaceIdSegment, relativePath].join(''), + opts.kibanaBaseUrl + ); + + return { + absoluteUrl: ruleUrl.toString(), + kibanaBaseUrl: opts.kibanaBaseUrl, + basePathname: basePathnamePrefix, + spaceIdSegment, + relativePath, + }; + } catch (error) { + opts.logger.debug( + `Rule "${opts.rule.id}" encountered an error while constructing the rule.url variable: ${error.message}` + ); + return; + } +}; diff --git a/x-pack/plugins/alerting/server/task_runner/action_scheduler/lib/format_action_to_enqueue.test.ts b/x-pack/plugins/alerting/server/task_runner/action_scheduler/lib/format_action_to_enqueue.test.ts new file mode 100644 index 0000000000000..02ff513c5b639 --- /dev/null +++ b/x-pack/plugins/alerting/server/task_runner/action_scheduler/lib/format_action_to_enqueue.test.ts @@ -0,0 +1,222 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { RULE_SAVED_OBJECT_TYPE } from '../../..'; +import { formatActionToEnqueue } from './format_action_to_enqueue'; + +describe('formatActionToEnqueue', () => { + test('should format a rule action as expected', () => { + expect( + formatActionToEnqueue({ + action: { + id: '1', + group: 'default', + actionTypeId: 'test', + params: { + foo: true, + contextVal: 'My {{context.value}} goes here', + stateVal: 'My {{state.value}} goes here', + alertVal: + 'My {{rule.id}} {{rule.name}} {{rule.spaceId}} {{rule.tags}} {{alert.id}} goes here', + }, + uuid: '111-111', + }, + apiKey: 'MTIzOmFiYw==', + executionId: '123', + ruleConsumer: 'rule-consumer', + ruleId: 'aaa', + ruleTypeId: 'security-rule', + spaceId: 'default', + }) + ).toEqual({ + id: '1', + uuid: '111-111', + params: { + foo: true, + contextVal: 'My {{context.value}} goes here', + stateVal: 'My {{state.value}} goes here', + alertVal: + 'My {{rule.id}} {{rule.name}} {{rule.spaceId}} {{rule.tags}} {{alert.id}} goes here', + }, + spaceId: 'default', + apiKey: 'MTIzOmFiYw==', + consumer: 'rule-consumer', + source: { + source: { + id: 'aaa', + type: RULE_SAVED_OBJECT_TYPE, + }, + type: 'SAVED_OBJECT', + }, + executionId: '123', + relatedSavedObjects: [ + { + id: 'aaa', + type: RULE_SAVED_OBJECT_TYPE, + namespace: undefined, + typeId: 'security-rule', + }, + ], + actionTypeId: 'test', + }); + }); + + test('should format a rule action with null apiKey as expected', () => { + expect( + formatActionToEnqueue({ + action: { + id: '1', + group: 'default', + actionTypeId: 'test', + params: { + foo: true, + contextVal: 'My {{context.value}} goes here', + stateVal: 'My {{state.value}} goes here', + alertVal: + 'My {{rule.id}} {{rule.name}} {{rule.spaceId}} {{rule.tags}} {{alert.id}} goes here', + }, + uuid: '111-111', + }, + apiKey: null, + executionId: '123', + ruleConsumer: 'rule-consumer', + ruleId: 'aaa', + ruleTypeId: 'security-rule', + spaceId: 'default', + }) + ).toEqual({ + id: '1', + uuid: '111-111', + params: { + foo: true, + contextVal: 'My {{context.value}} goes here', + stateVal: 'My {{state.value}} goes here', + alertVal: + 'My {{rule.id}} {{rule.name}} {{rule.spaceId}} {{rule.tags}} {{alert.id}} goes here', + }, + spaceId: 'default', + apiKey: null, + consumer: 'rule-consumer', + source: { + source: { + id: 'aaa', + type: RULE_SAVED_OBJECT_TYPE, + }, + type: 'SAVED_OBJECT', + }, + executionId: '123', + relatedSavedObjects: [ + { + id: 'aaa', + type: RULE_SAVED_OBJECT_TYPE, + namespace: undefined, + typeId: 'security-rule', + }, + ], + actionTypeId: 'test', + }); + }); + + test('should format a rule action in a custom space as expected', () => { + expect( + formatActionToEnqueue({ + action: { + id: '1', + group: 'default', + actionTypeId: 'test', + params: { + foo: true, + contextVal: 'My {{context.value}} goes here', + stateVal: 'My {{state.value}} goes here', + alertVal: + 'My {{rule.id}} {{rule.name}} {{rule.spaceId}} {{rule.tags}} {{alert.id}} goes here', + }, + uuid: '111-111', + }, + apiKey: 'MTIzOmFiYw==', + executionId: '123', + ruleConsumer: 'rule-consumer', + ruleId: 'aaa', + ruleTypeId: 'security-rule', + spaceId: 'my-special-space', + }) + ).toEqual({ + id: '1', + uuid: '111-111', + params: { + foo: true, + contextVal: 'My {{context.value}} goes here', + stateVal: 'My {{state.value}} goes here', + alertVal: + 'My {{rule.id}} {{rule.name}} {{rule.spaceId}} {{rule.tags}} {{alert.id}} goes here', + }, + spaceId: 'my-special-space', + apiKey: 'MTIzOmFiYw==', + consumer: 'rule-consumer', + source: { + source: { + id: 'aaa', + type: RULE_SAVED_OBJECT_TYPE, + }, + type: 'SAVED_OBJECT', + }, + executionId: '123', + relatedSavedObjects: [ + { + id: 'aaa', + type: RULE_SAVED_OBJECT_TYPE, + namespace: 'my-special-space', + typeId: 'security-rule', + }, + ], + actionTypeId: 'test', + }); + }); + + test('should format a system action as expected', () => { + expect( + formatActionToEnqueue({ + action: { + id: '1', + actionTypeId: '.test-system-action', + params: { myParams: 'test' }, + uuid: 'xxxyyyyzzzz', + }, + apiKey: 'MTIzOmFiYw==', + executionId: '123', + ruleConsumer: 'rule-consumer', + ruleId: 'aaa', + ruleTypeId: 'security-rule', + spaceId: 'default', + }) + ).toEqual({ + id: '1', + uuid: 'xxxyyyyzzzz', + params: { myParams: 'test' }, + spaceId: 'default', + apiKey: 'MTIzOmFiYw==', + consumer: 'rule-consumer', + source: { + source: { + id: 'aaa', + type: RULE_SAVED_OBJECT_TYPE, + }, + type: 'SAVED_OBJECT', + }, + executionId: '123', + relatedSavedObjects: [ + { + id: 'aaa', + type: RULE_SAVED_OBJECT_TYPE, + namespace: undefined, + typeId: 'security-rule', + }, + ], + actionTypeId: '.test-system-action', + }); + }); +}); diff --git a/x-pack/plugins/alerting/server/task_runner/action_scheduler/lib/format_action_to_enqueue.ts b/x-pack/plugins/alerting/server/task_runner/action_scheduler/lib/format_action_to_enqueue.ts new file mode 100644 index 0000000000000..af560a19ab9be --- /dev/null +++ b/x-pack/plugins/alerting/server/task_runner/action_scheduler/lib/format_action_to_enqueue.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 { RuleAction, RuleSystemAction } from '@kbn/alerting-types'; +import { asSavedObjectExecutionSource } from '@kbn/actions-plugin/server'; +import { RULE_SAVED_OBJECT_TYPE } from '../../..'; + +interface FormatActionToEnqueueOpts { + action: RuleAction | RuleSystemAction; + apiKey: string | null; + executionId: string; + ruleConsumer: string; + ruleId: string; + ruleTypeId: string; + spaceId: string; +} + +export const formatActionToEnqueue = (opts: FormatActionToEnqueueOpts) => { + const { action, apiKey, executionId, ruleConsumer, ruleId, ruleTypeId, spaceId } = opts; + + const namespace = spaceId === 'default' ? {} : { namespace: spaceId }; + return { + id: action.id, + uuid: action.uuid, + params: action.params, + spaceId, + apiKey: apiKey ?? null, + consumer: ruleConsumer, + source: asSavedObjectExecutionSource({ + id: ruleId, + type: RULE_SAVED_OBJECT_TYPE, + }), + executionId, + relatedSavedObjects: [ + { + id: ruleId, + type: RULE_SAVED_OBJECT_TYPE, + namespace: namespace.namespace, + typeId: ruleTypeId, + }, + ], + actionTypeId: action.actionTypeId, + }; +}; diff --git a/x-pack/plugins/alerting/server/task_runner/action_scheduler/get_summarized_alerts.test.ts b/x-pack/plugins/alerting/server/task_runner/action_scheduler/lib/get_summarized_alerts.test.ts similarity index 95% rename from x-pack/plugins/alerting/server/task_runner/action_scheduler/get_summarized_alerts.test.ts rename to x-pack/plugins/alerting/server/task_runner/action_scheduler/lib/get_summarized_alerts.test.ts index 9afd0647094eb..036c49c51d1be 100644 --- a/x-pack/plugins/alerting/server/task_runner/action_scheduler/get_summarized_alerts.test.ts +++ b/x-pack/plugins/alerting/server/task_runner/action_scheduler/lib/get_summarized_alerts.test.ts @@ -6,10 +6,10 @@ */ import { getSummarizedAlerts } from './get_summarized_alerts'; -import { alertsClientMock } from '../../alerts_client/alerts_client.mock'; -import { mockAAD } from '../fixtures'; +import { alertsClientMock } from '../../../alerts_client/alerts_client.mock'; +import { mockAAD } from '../../fixtures'; import { ALERT_UUID } from '@kbn/rule-data-utils'; -import { generateAlert } from './test_fixtures'; +import { generateAlert } from '../test_fixtures'; import { getErrorSource } from '@kbn/task-manager-plugin/server/task_running'; const alertsClient = alertsClientMock.create(); diff --git a/x-pack/plugins/alerting/server/task_runner/action_scheduler/get_summarized_alerts.ts b/x-pack/plugins/alerting/server/task_runner/action_scheduler/lib/get_summarized_alerts.ts similarity index 98% rename from x-pack/plugins/alerting/server/task_runner/action_scheduler/get_summarized_alerts.ts rename to x-pack/plugins/alerting/server/task_runner/action_scheduler/lib/get_summarized_alerts.ts index df667a3e20775..00e155856d946 100644 --- a/x-pack/plugins/alerting/server/task_runner/action_scheduler/get_summarized_alerts.ts +++ b/x-pack/plugins/alerting/server/task_runner/action_scheduler/lib/get_summarized_alerts.ts @@ -7,13 +7,13 @@ import { ALERT_UUID } from '@kbn/rule-data-utils'; import { createTaskRunError, TaskErrorSource } from '@kbn/task-manager-plugin/server'; -import { GetSummarizedAlertsParams, IAlertsClient } from '../../alerts_client/types'; +import { GetSummarizedAlertsParams, IAlertsClient } from '../../../alerts_client/types'; import { AlertInstanceContext, AlertInstanceState, CombinedSummarizedAlerts, RuleAlertData, -} from '../../types'; +} from '../../../types'; interface GetSummarizedAlertsOpts< State extends AlertInstanceState, diff --git a/x-pack/plugins/alerting/server/task_runner/action_scheduler/lib/index.ts b/x-pack/plugins/alerting/server/task_runner/action_scheduler/lib/index.ts new file mode 100644 index 0000000000000..1bd78f302d00c --- /dev/null +++ b/x-pack/plugins/alerting/server/task_runner/action_scheduler/lib/index.ts @@ -0,0 +1,20 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +export { buildRuleUrl } from './build_rule_url'; +export { formatActionToEnqueue } from './format_action_to_enqueue'; +export { getSummarizedAlerts } from './get_summarized_alerts'; +export { + isSummaryAction, + isActionOnInterval, + isSummaryActionThrottled, + generateActionHash, + getSummaryActionsFromTaskState, + getSummaryActionTimeBounds, + logNumberOfFilteredAlerts, +} from './rule_action_helper'; +export { shouldScheduleAction } from './should_schedule_action'; diff --git a/x-pack/plugins/alerting/server/task_runner/action_scheduler/rule_action_helper.test.ts b/x-pack/plugins/alerting/server/task_runner/action_scheduler/lib/rule_action_helper.test.ts similarity index 99% rename from x-pack/plugins/alerting/server/task_runner/action_scheduler/rule_action_helper.test.ts rename to x-pack/plugins/alerting/server/task_runner/action_scheduler/lib/rule_action_helper.test.ts index cc8a0a1b0cde5..1adb68a951351 100644 --- a/x-pack/plugins/alerting/server/task_runner/action_scheduler/rule_action_helper.test.ts +++ b/x-pack/plugins/alerting/server/task_runner/action_scheduler/lib/rule_action_helper.test.ts @@ -7,7 +7,7 @@ import { Logger } from '@kbn/logging'; import { loggingSystemMock } from '@kbn/core/server/mocks'; -import { RuleAction } from '../../types'; +import { RuleAction } from '../../../types'; import { generateActionHash, getSummaryActionsFromTaskState, diff --git a/x-pack/plugins/alerting/server/task_runner/action_scheduler/rule_action_helper.ts b/x-pack/plugins/alerting/server/task_runner/action_scheduler/lib/rule_action_helper.ts similarity index 99% rename from x-pack/plugins/alerting/server/task_runner/action_scheduler/rule_action_helper.ts rename to x-pack/plugins/alerting/server/task_runner/action_scheduler/lib/rule_action_helper.ts index 67223b0728689..c3ef79b3086d8 100644 --- a/x-pack/plugins/alerting/server/task_runner/action_scheduler/rule_action_helper.ts +++ b/x-pack/plugins/alerting/server/task_runner/action_scheduler/lib/rule_action_helper.ts @@ -12,7 +12,7 @@ import { RuleAction, RuleNotifyWhenTypeValues, ThrottledActions, -} from '../../../common'; +} from '../../../../common'; export const isSummaryAction = (action?: RuleAction) => { return action?.frequency?.summary ?? false; diff --git a/x-pack/plugins/alerting/server/task_runner/action_scheduler/lib/should_schedule_action.test.ts b/x-pack/plugins/alerting/server/task_runner/action_scheduler/lib/should_schedule_action.test.ts new file mode 100644 index 0000000000000..7ebd65fab005d --- /dev/null +++ b/x-pack/plugins/alerting/server/task_runner/action_scheduler/lib/should_schedule_action.test.ts @@ -0,0 +1,195 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { loggingSystemMock } from '@kbn/core-logging-server-mocks'; +import { shouldScheduleAction } from './should_schedule_action'; +import { ruleRunMetricsStoreMock } from '../../../lib/rule_run_metrics_store.mock'; +import { ActionsCompletion } from '@kbn/alerting-state-types'; + +const logger = loggingSystemMock.create().get(); +const ruleRunMetricsStore = ruleRunMetricsStoreMock.create(); + +describe('shouldScheduleAction', () => { + beforeEach(() => { + jest.clearAllMocks(); + }); + + test('should return false if the the limit of executable actions has been reached', () => { + ruleRunMetricsStore.hasReachedTheExecutableActionsLimit.mockReturnValueOnce(true); + expect( + shouldScheduleAction({ + action: { + id: '1', + group: 'default', + actionTypeId: 'test-action-type-id', + params: { + foo: true, + contextVal: 'My {{context.value}} goes here', + stateVal: 'My {{state.value}} goes here', + alertVal: + 'My {{rule.id}} {{rule.name}} {{rule.spaceId}} {{rule.tags}} {{alert.id}} goes here', + }, + uuid: '111-111', + }, + actionsConfigMap: { + default: { max: 4 }, + 'test-action-type-id': { max: 2 }, + }, + isActionExecutable: () => true, + logger, + ruleId: '1', + ruleRunMetricsStore, + }) + ).toEqual(false); + + expect(ruleRunMetricsStore.setTriggeredActionsStatusByConnectorType).toHaveBeenCalledWith({ + actionTypeId: 'test-action-type-id', + status: ActionsCompletion.PARTIAL, + }); + expect(logger.debug).toHaveBeenCalledWith( + `Rule "1" skipped scheduling action "1" because the maximum number of allowed actions has been reached.` + ); + }); + + test('should return false if the the limit of executable actions for this action type has been reached', () => { + ruleRunMetricsStore.hasReachedTheExecutableActionsLimitByConnectorType.mockReturnValueOnce( + true + ); + ruleRunMetricsStore.hasConnectorTypeReachedTheLimit.mockReturnValueOnce(true); + expect( + shouldScheduleAction({ + action: { + id: '1', + group: 'default', + actionTypeId: 'test-action-type-id', + params: { + foo: true, + contextVal: 'My {{context.value}} goes here', + stateVal: 'My {{state.value}} goes here', + alertVal: + 'My {{rule.id}} {{rule.name}} {{rule.spaceId}} {{rule.tags}} {{alert.id}} goes here', + }, + uuid: '111-111', + }, + actionsConfigMap: { + default: { max: 4 }, + 'test-action-type-id': { max: 2 }, + }, + isActionExecutable: () => true, + logger, + ruleId: '1', + ruleRunMetricsStore, + }) + ).toEqual(false); + + expect(ruleRunMetricsStore.setTriggeredActionsStatusByConnectorType).toHaveBeenCalledWith({ + actionTypeId: 'test-action-type-id', + status: ActionsCompletion.PARTIAL, + }); + expect(logger.debug).not.toHaveBeenCalled(); + }); + + test('should return false and log if the the limit of executable actions for this action type has been reached', () => { + ruleRunMetricsStore.hasReachedTheExecutableActionsLimitByConnectorType.mockReturnValueOnce( + true + ); + ruleRunMetricsStore.hasConnectorTypeReachedTheLimit.mockReturnValueOnce(false); + expect( + shouldScheduleAction({ + action: { + id: '1', + group: 'default', + actionTypeId: 'test-action-type-id', + params: { + foo: true, + contextVal: 'My {{context.value}} goes here', + stateVal: 'My {{state.value}} goes here', + alertVal: + 'My {{rule.id}} {{rule.name}} {{rule.spaceId}} {{rule.tags}} {{alert.id}} goes here', + }, + uuid: '111-111', + }, + actionsConfigMap: { + default: { max: 4 }, + 'test-action-type-id': { max: 2 }, + }, + isActionExecutable: () => true, + logger, + ruleId: '1', + ruleRunMetricsStore, + }) + ).toEqual(false); + + expect(ruleRunMetricsStore.setTriggeredActionsStatusByConnectorType).toHaveBeenCalledWith({ + actionTypeId: 'test-action-type-id', + status: ActionsCompletion.PARTIAL, + }); + expect(logger.debug).toHaveBeenCalledWith( + `Rule "1" skipped scheduling action "1" because the maximum number of allowed actions for connector type test-action-type-id has been reached.` + ); + }); + + test('should return false the action is not executable', () => { + expect( + shouldScheduleAction({ + action: { + id: '1', + group: 'default', + actionTypeId: 'test-action-type-id', + params: { + foo: true, + contextVal: 'My {{context.value}} goes here', + stateVal: 'My {{state.value}} goes here', + alertVal: + 'My {{rule.id}} {{rule.name}} {{rule.spaceId}} {{rule.tags}} {{alert.id}} goes here', + }, + uuid: '111-111', + }, + actionsConfigMap: { + default: { max: 4 }, + 'test-action-type-id': { max: 2 }, + }, + isActionExecutable: () => false, + logger, + ruleId: '1', + ruleRunMetricsStore, + }) + ).toEqual(false); + + expect(logger.warn).toHaveBeenCalledWith( + `Rule "1" skipped scheduling action "1" because it is disabled` + ); + }); + + test('should return true if the action is executable and no limits have been reached', () => { + expect( + shouldScheduleAction({ + action: { + id: '1', + group: 'default', + actionTypeId: 'test-action-type-id', + params: { + foo: true, + contextVal: 'My {{context.value}} goes here', + stateVal: 'My {{state.value}} goes here', + alertVal: + 'My {{rule.id}} {{rule.name}} {{rule.spaceId}} {{rule.tags}} {{alert.id}} goes here', + }, + uuid: '111-111', + }, + actionsConfigMap: { + default: { max: 4 }, + 'test-action-type-id': { max: 2 }, + }, + isActionExecutable: () => true, + logger, + ruleId: '1', + ruleRunMetricsStore, + }) + ).toEqual(true); + }); +}); diff --git a/x-pack/plugins/alerting/server/task_runner/action_scheduler/lib/should_schedule_action.ts b/x-pack/plugins/alerting/server/task_runner/action_scheduler/lib/should_schedule_action.ts new file mode 100644 index 0000000000000..99fa3c42ad3df --- /dev/null +++ b/x-pack/plugins/alerting/server/task_runner/action_scheduler/lib/should_schedule_action.ts @@ -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 { Logger } from '@kbn/logging'; +import { ActionsCompletion } from '@kbn/alerting-state-types'; +import { RuleAction, RuleSystemAction } from '@kbn/alerting-types'; +import { RuleRunMetricsStore } from '../../../lib/rule_run_metrics_store'; +import { ActionsConfigMap } from '../../../lib/get_actions_config_map'; + +interface ShouldScheduleActionOpts { + action: RuleAction | RuleSystemAction; + actionsConfigMap: ActionsConfigMap; + isActionExecutable( + actionId: string, + actionTypeId: string, + options?: { notifyUsage: boolean } + ): boolean; + logger: Logger; + ruleId: string; + ruleRunMetricsStore: RuleRunMetricsStore; +} + +export const shouldScheduleAction = (opts: ShouldScheduleActionOpts): boolean => { + const { actionsConfigMap, action, logger, ruleRunMetricsStore } = opts; + + // keep track of how many actions we want to schedule by connector type + ruleRunMetricsStore.incrementNumberOfGeneratedActionsByConnectorType(action.actionTypeId); + + if (ruleRunMetricsStore.hasReachedTheExecutableActionsLimit(actionsConfigMap)) { + ruleRunMetricsStore.setTriggeredActionsStatusByConnectorType({ + actionTypeId: action.actionTypeId, + status: ActionsCompletion.PARTIAL, + }); + logger.debug( + `Rule "${opts.ruleId}" skipped scheduling action "${action.id}" because the maximum number of allowed actions has been reached.` + ); + return false; + } + + if ( + ruleRunMetricsStore.hasReachedTheExecutableActionsLimitByConnectorType({ + actionTypeId: action.actionTypeId, + actionsConfigMap, + }) + ) { + if (!ruleRunMetricsStore.hasConnectorTypeReachedTheLimit(action.actionTypeId)) { + logger.debug( + `Rule "${opts.ruleId}" skipped scheduling action "${action.id}" because the maximum number of allowed actions for connector type ${action.actionTypeId} has been reached.` + ); + } + ruleRunMetricsStore.setTriggeredActionsStatusByConnectorType({ + actionTypeId: action.actionTypeId, + status: ActionsCompletion.PARTIAL, + }); + return false; + } + + if (!opts.isActionExecutable(action.id, action.actionTypeId, { notifyUsage: true })) { + logger.warn( + `Rule "${opts.ruleId}" skipped scheduling action "${action.id}" because it is disabled` + ); + return false; + } + + return true; +}; diff --git a/x-pack/plugins/alerting/server/task_runner/action_scheduler/schedulers/per_alert_action_scheduler.test.ts b/x-pack/plugins/alerting/server/task_runner/action_scheduler/schedulers/per_alert_action_scheduler.test.ts index 53e75245d94d0..99a693133a2a6 100644 --- a/x-pack/plugins/alerting/server/task_runner/action_scheduler/schedulers/per_alert_action_scheduler.test.ts +++ b/x-pack/plugins/alerting/server/task_runner/action_scheduler/schedulers/per_alert_action_scheduler.test.ts @@ -16,6 +16,12 @@ import { PerAlertActionScheduler } from './per_alert_action_scheduler'; import { getRule, getRuleType, getDefaultSchedulerContext, generateAlert } from '../test_fixtures'; import { SanitizedRuleAction } from '@kbn/alerting-types'; import { ALERT_UUID } from '@kbn/rule-data-utils'; +import { Alert } from '../../../alert'; +import { + ActionsCompletion, + AlertInstanceContext, + AlertInstanceState, +} from '@kbn/alerting-state-types'; const alertingEventLogger = alertingEventLoggerMock.create(); const actionsClient = actionsClientMock.create(); @@ -25,9 +31,10 @@ const logger = loggingSystemMock.create().get(); let ruleRunMetricsStore: RuleRunMetricsStore; const rule = getRule({ + id: 'rule-id-1', actions: [ { - id: '1', + id: 'action-1', group: 'default', actionTypeId: 'test', frequency: { summary: false, notifyWhen: 'onActiveAlert', throttle: null }, @@ -41,7 +48,7 @@ const rule = getRule({ uuid: '111-111', }, { - id: '2', + id: 'action-2', group: 'default', actionTypeId: 'test', frequency: { summary: false, notifyWhen: 'onActiveAlert', throttle: null }, @@ -55,7 +62,7 @@ const rule = getRule({ uuid: '222-222', }, { - id: '3', + id: 'action-3', group: 'default', actionTypeId: 'test', frequency: { summary: true, notifyWhen: 'onActiveAlert' }, @@ -84,6 +91,21 @@ const getSchedulerContext = (params = {}) => { return { ...defaultSchedulerContext, rule, ...params, ruleRunMetricsStore }; }; +const getResult = (actionId: string, alertId: string, actionUuid: string) => ({ + actionToEnqueue: { + actionTypeId: 'test', + apiKey: 'MTIzOmFiYw==', + consumer: 'rule-consumer', + executionId: '5f6aa57d-3e22-484e-bae8-cbed868f4d28', + id: actionId, + uuid: actionUuid, + relatedSavedObjects: [{ id: 'rule-id-1', namespace: 'test1', type: 'alert', typeId: 'test' }], + source: { source: { id: 'rule-id-1', type: 'alert' }, type: 'SAVED_OBJECT' }, + spaceId: 'test1', + }, + actionToLog: { alertGroup: 'default', alertId, id: actionId, uuid: actionUuid, typeId: 'test' }, +}); + let clock: sinon.SinonFakeTimers; describe('Per-Alert Action Scheduler', () => { @@ -93,6 +115,7 @@ describe('Per-Alert Action Scheduler', () => { beforeEach(() => { jest.resetAllMocks(); + jest.clearAllMocks(); mockActionsPlugin.isActionTypeEnabled.mockReturnValue(true); mockActionsPlugin.isActionExecutable.mockReturnValue(true); mockActionsPlugin.getActionsClientWithRequest.mockResolvedValue(actionsClient); @@ -163,67 +186,93 @@ describe('Per-Alert Action Scheduler', () => { expect(scheduler.actions).toEqual([actions[0]]); expect(logger.error).toHaveBeenCalledTimes(1); expect(logger.error).toHaveBeenCalledWith( - `Skipping action \"2\" for rule \"1\" because the rule type \"Test\" does not support alert-as-data.` + `Skipping action \"2\" for rule \"rule-id-1\" because the rule type \"Test\" does not support alert-as-data.` ); }); - describe('generateExecutables', () => { - const newAlert1 = generateAlert({ id: 1 }); - const newAlert2 = generateAlert({ id: 2 }); - const alerts = { ...newAlert1, ...newAlert2 }; + describe('getActionsToSchedule', () => { + let newAlert1: Record< + string, + Alert + >; + let newAlert2: Record< + string, + Alert + >; + let alerts: Record< + string, + Alert + >; + + beforeEach(() => { + newAlert1 = generateAlert({ id: 1 }); + newAlert2 = generateAlert({ id: 2 }); + alerts = { ...newAlert1, ...newAlert2 }; + }); - test('should generate executable for each alert and each action', async () => { + test('should create action to schedule for each alert and each action', async () => { + // 2 per-alert actions * 2 alerts = 4 actions to schedule const scheduler = new PerAlertActionScheduler(getSchedulerContext()); - const executables = await scheduler.generateExecutables({ - alerts, - throttledSummaryActions: {}, - }); + const results = await scheduler.getActionsToSchedule({ alerts }); expect(alertsClient.getSummarizedAlerts).not.toHaveBeenCalled(); expect(logger.debug).not.toHaveBeenCalled(); - expect(executables).toHaveLength(4); + expect(ruleRunMetricsStore.getNumberOfGeneratedActions()).toEqual(4); + expect(ruleRunMetricsStore.getNumberOfTriggeredActions()).toEqual(4); + expect(ruleRunMetricsStore.getStatusByConnectorType('test')).toEqual({ + numberOfGeneratedActions: 4, + numberOfTriggeredActions: 4, + }); - expect(executables).toEqual([ - { action: rule.actions[0], alert: alerts['1'] }, - { action: rule.actions[0], alert: alerts['2'] }, - { action: rule.actions[1], alert: alerts['1'] }, - { action: rule.actions[1], alert: alerts['2'] }, + expect(results).toHaveLength(4); + expect(results).toEqual([ + getResult('action-1', '1', '111-111'), + getResult('action-1', '2', '111-111'), + getResult('action-2', '1', '222-222'), + getResult('action-2', '2', '222-222'), ]); }); - test('should skip generating executable when alert has maintenance window', async () => { + test('should skip creating actions to schedule when alert has maintenance window', async () => { + // 2 per-alert actions * 2 alerts = 4 actions to schedule + // but alert 1 has maintenance window, so only actions for alert 2 should be scheduled const scheduler = new PerAlertActionScheduler(getSchedulerContext()); const newAlertWithMaintenanceWindow = generateAlert({ id: 1, maintenanceWindowIds: ['mw-1'], }); const alertsWithMaintenanceWindow = { ...newAlertWithMaintenanceWindow, ...newAlert2 }; - const executables = await scheduler.generateExecutables({ - alerts: alertsWithMaintenanceWindow, - throttledSummaryActions: {}, - }); + const results = await scheduler.getActionsToSchedule({ alerts: alertsWithMaintenanceWindow }); expect(alertsClient.getSummarizedAlerts).not.toHaveBeenCalled(); expect(logger.debug).toHaveBeenCalledTimes(2); expect(logger.debug).toHaveBeenNthCalledWith( 1, - `no scheduling of summary actions \"1\" for rule \"1\": has active maintenance windows mw-1.` + `no scheduling of summary actions \"action-1\" for rule \"rule-id-1\": has active maintenance windows mw-1.` ); expect(logger.debug).toHaveBeenNthCalledWith( 2, - `no scheduling of summary actions \"2\" for rule \"1\": has active maintenance windows mw-1.` + `no scheduling of summary actions \"action-2\" for rule \"rule-id-1\": has active maintenance windows mw-1.` ); - expect(executables).toHaveLength(2); + expect(ruleRunMetricsStore.getNumberOfGeneratedActions()).toEqual(2); + expect(ruleRunMetricsStore.getNumberOfTriggeredActions()).toEqual(2); + expect(ruleRunMetricsStore.getStatusByConnectorType('test')).toEqual({ + numberOfGeneratedActions: 2, + numberOfTriggeredActions: 2, + }); - expect(executables).toEqual([ - { action: rule.actions[0], alert: alerts['2'] }, - { action: rule.actions[1], alert: alerts['2'] }, + expect(results).toHaveLength(2); + expect(results).toEqual([ + getResult('action-1', '2', '111-111'), + getResult('action-2', '2', '222-222'), ]); }); - test('should skip generating executable when alert has invalid action group', async () => { + test('should skip creating actions to schedule when alert has invalid action group', async () => { + // 2 per-alert actions * 2 alerts = 4 actions to schedule + // but alert 1 has invalid action group, so only actions for alert 2 should be scheduled const scheduler = new PerAlertActionScheduler(getSchedulerContext()); const newAlertInvalidActionGroup = generateAlert({ id: 1, @@ -231,9 +280,8 @@ describe('Per-Alert Action Scheduler', () => { group: 'invalid', }); const alertsWithInvalidActionGroup = { ...newAlertInvalidActionGroup, ...newAlert2 }; - const executables = await scheduler.generateExecutables({ + const results = await scheduler.getActionsToSchedule({ alerts: alertsWithInvalidActionGroup, - throttledSummaryActions: {}, }); expect(alertsClient.getSummarizedAlerts).not.toHaveBeenCalled(); @@ -247,15 +295,23 @@ describe('Per-Alert Action Scheduler', () => { `Invalid action group \"invalid\" for rule \"test\".` ); - expect(executables).toHaveLength(2); + expect(ruleRunMetricsStore.getNumberOfGeneratedActions()).toEqual(2); + expect(ruleRunMetricsStore.getNumberOfTriggeredActions()).toEqual(2); + expect(ruleRunMetricsStore.getStatusByConnectorType('test')).toEqual({ + numberOfGeneratedActions: 2, + numberOfTriggeredActions: 2, + }); - expect(executables).toEqual([ - { action: rule.actions[0], alert: alerts['2'] }, - { action: rule.actions[1], alert: alerts['2'] }, + expect(results).toHaveLength(2); + expect(results).toEqual([ + getResult('action-1', '2', '111-111'), + getResult('action-2', '2', '222-222'), ]); }); - test('should skip generating executable when alert has pending recovered count greater than 0 and notifyWhen is onActiveAlert', async () => { + test('should skip creating actions to schedule when alert has pending recovered count greater than 0 and notifyWhen is onActiveAlert', async () => { + // 2 per-alert actions * 2 alerts = 4 actions to schedule + // but alert 1 has a pending recovered count > 0 & notifyWhen is onActiveAlert, so only actions for alert 2 should be scheduled const scheduler = new PerAlertActionScheduler(getSchedulerContext()); const newAlertWithPendingRecoveredCount = generateAlert({ id: 1, @@ -265,23 +321,31 @@ describe('Per-Alert Action Scheduler', () => { ...newAlertWithPendingRecoveredCount, ...newAlert2, }; - const executables = await scheduler.generateExecutables({ + const results = await scheduler.getActionsToSchedule({ alerts: alertsWithPendingRecoveredCount, - throttledSummaryActions: {}, }); expect(alertsClient.getSummarizedAlerts).not.toHaveBeenCalled(); - expect(executables).toHaveLength(2); - expect(executables).toEqual([ - { action: rule.actions[0], alert: alerts['2'] }, - { action: rule.actions[1], alert: alerts['2'] }, + expect(ruleRunMetricsStore.getNumberOfGeneratedActions()).toEqual(2); + expect(ruleRunMetricsStore.getNumberOfTriggeredActions()).toEqual(2); + expect(ruleRunMetricsStore.getStatusByConnectorType('test')).toEqual({ + numberOfGeneratedActions: 2, + numberOfTriggeredActions: 2, + }); + + expect(results).toHaveLength(2); + expect(results).toEqual([ + getResult('action-1', '2', '111-111'), + getResult('action-2', '2', '222-222'), ]); }); - test('should skip generating executable when alert has pending recovered count greater than 0 and notifyWhen is onThrottleInterval', async () => { + test('should skip creating actions to schedule when alert has pending recovered count greater than 0 and notifyWhen is onThrottleInterval', async () => { + // 2 per-alert actions * 2 alerts = 4 actions to schedule + // but alert 1 has a pending recovered count > 0 & notifyWhen is onThrottleInterval, so only actions for alert 2 should be scheduled const onThrottleIntervalAction: SanitizedRuleAction = { - id: '2', + id: 'action-4', group: 'default', actionTypeId: 'test', frequency: { summary: false, notifyWhen: 'onThrottleInterval', throttle: '1h' }, @@ -292,43 +356,45 @@ describe('Per-Alert Action Scheduler', () => { alertVal: 'My {{rule.id}} {{rule.name}} {{rule.spaceId}} {{rule.tags}} {{alert.id}} goes here', }, - uuid: '222-222', + uuid: '444-444', }; const scheduler = new PerAlertActionScheduler({ ...getSchedulerContext(), rule: { ...rule, actions: [rule.actions[0], onThrottleIntervalAction] }, }); - const newAlertWithPendingRecoveredCount = generateAlert({ - id: 1, - pendingRecoveredCount: 3, - }); + const newAlertWithPendingRecoveredCount = generateAlert({ id: 1, pendingRecoveredCount: 3 }); const alertsWithPendingRecoveredCount = { ...newAlertWithPendingRecoveredCount, ...newAlert2, }; - const executables = await scheduler.generateExecutables({ + const results = await scheduler.getActionsToSchedule({ alerts: alertsWithPendingRecoveredCount, - throttledSummaryActions: {}, }); expect(alertsClient.getSummarizedAlerts).not.toHaveBeenCalled(); - expect(executables).toHaveLength(2); - expect(executables).toEqual([ - { action: rule.actions[0], alert: alerts['2'] }, - { action: onThrottleIntervalAction, alert: alerts['2'] }, + expect(ruleRunMetricsStore.getNumberOfGeneratedActions()).toEqual(2); + expect(ruleRunMetricsStore.getNumberOfTriggeredActions()).toEqual(2); + expect(ruleRunMetricsStore.getStatusByConnectorType('test')).toEqual({ + numberOfGeneratedActions: 2, + numberOfTriggeredActions: 2, + }); + + expect(results).toHaveLength(2); + expect(results).toEqual([ + getResult('action-1', '2', '111-111'), + getResult('action-4', '2', '444-444'), ]); }); - test('should skip generating executable when alert is muted', async () => { + test('should skip creating actions to schedule when alert is muted', async () => { + // 2 per-alert actions * 2 alerts = 4 actions to schedule + // but alert 2 is muted, so only actions for alert 1 should be scheduled const scheduler = new PerAlertActionScheduler({ ...getSchedulerContext(), rule: { ...rule, mutedInstanceIds: ['2'] }, }); - const executables = await scheduler.generateExecutables({ - alerts, - throttledSummaryActions: {}, - }); + const results = await scheduler.getActionsToSchedule({ alerts }); expect(alertsClient.getSummarizedAlerts).not.toHaveBeenCalled(); expect(logger.debug).toHaveBeenCalledTimes(1); @@ -336,20 +402,27 @@ describe('Per-Alert Action Scheduler', () => { 1, `skipping scheduling of actions for '2' in rule rule-label: rule is muted` ); - expect(executables).toHaveLength(2); - // @ts-expect-error private variable - expect(scheduler.skippedAlerts).toEqual({ '2': { reason: 'muted' } }); + expect(ruleRunMetricsStore.getNumberOfGeneratedActions()).toEqual(2); + expect(ruleRunMetricsStore.getNumberOfTriggeredActions()).toEqual(2); + expect(ruleRunMetricsStore.getStatusByConnectorType('test')).toEqual({ + numberOfGeneratedActions: 2, + numberOfTriggeredActions: 2, + }); - expect(executables).toEqual([ - { action: rule.actions[0], alert: alerts['1'] }, - { action: rule.actions[1], alert: alerts['1'] }, + expect(results).toHaveLength(2); + expect(results).toEqual([ + getResult('action-1', '1', '111-111'), + getResult('action-2', '1', '222-222'), ]); + + // @ts-expect-error private variable + expect(scheduler.skippedAlerts).toEqual({ '2': { reason: 'muted' } }); }); - test('should skip generating executable when alert action group has not changed and notifyWhen is onActionGroupChange', async () => { + test('should skip creating actions to schedule when alert action group has not changed and notifyWhen is onActionGroupChange', async () => { const onActionGroupChangeAction: SanitizedRuleAction = { - id: '2', + id: 'action-4', group: 'default', actionTypeId: 'test', frequency: { summary: false, notifyWhen: 'onActionGroupChange', throttle: null }, @@ -360,7 +433,7 @@ describe('Per-Alert Action Scheduler', () => { alertVal: 'My {{rule.id}} {{rule.name}} {{rule.spaceId}} {{rule.tags}} {{alert.id}} goes here', }, - uuid: '222-222', + uuid: '444-444', }; const activeAlert1 = generateAlert({ @@ -380,10 +453,7 @@ describe('Per-Alert Action Scheduler', () => { rule: { ...rule, actions: [rule.actions[0], onActionGroupChangeAction] }, }); - const executables = await scheduler.generateExecutables({ - alerts: alertsWithOngoingAlert, - throttledSummaryActions: {}, - }); + const results = await scheduler.getActionsToSchedule({ alerts: alertsWithOngoingAlert }); expect(alertsClient.getSummarizedAlerts).not.toHaveBeenCalled(); expect(logger.debug).toHaveBeenCalledTimes(1); @@ -391,21 +461,28 @@ describe('Per-Alert Action Scheduler', () => { 1, `skipping scheduling of actions for '2' in rule rule-label: alert is active but action group has not changed` ); - expect(executables).toHaveLength(3); - // @ts-expect-error private variable - expect(scheduler.skippedAlerts).toEqual({ '2': { reason: 'actionGroupHasNotChanged' } }); + expect(ruleRunMetricsStore.getNumberOfGeneratedActions()).toEqual(3); + expect(ruleRunMetricsStore.getNumberOfTriggeredActions()).toEqual(3); + expect(ruleRunMetricsStore.getStatusByConnectorType('test')).toEqual({ + numberOfGeneratedActions: 3, + numberOfTriggeredActions: 3, + }); - expect(executables).toEqual([ - { action: rule.actions[0], alert: alertsWithOngoingAlert['1'] }, - { action: rule.actions[0], alert: alertsWithOngoingAlert['2'] }, - { action: onActionGroupChangeAction, alert: alertsWithOngoingAlert['1'] }, + expect(results).toHaveLength(3); + expect(results).toEqual([ + getResult('action-1', '1', '111-111'), + getResult('action-1', '2', '111-111'), + getResult('action-4', '1', '444-444'), ]); + + // @ts-expect-error private variable + expect(scheduler.skippedAlerts).toEqual({ '2': { reason: 'actionGroupHasNotChanged' } }); }); - test('should skip generating executable when throttle interval has not passed and notifyWhen is onThrottleInterval', async () => { + test('should skip creating actions to schedule when throttle interval has not passed and notifyWhen is onThrottleInterval', async () => { const onThrottleIntervalAction: SanitizedRuleAction = { - id: '2', + id: 'action-5', group: 'default', actionTypeId: 'test', frequency: { summary: false, notifyWhen: 'onThrottleInterval', throttle: '1h' }, @@ -416,13 +493,13 @@ describe('Per-Alert Action Scheduler', () => { alertVal: 'My {{rule.id}} {{rule.name}} {{rule.spaceId}} {{rule.tags}} {{alert.id}} goes here', }, - uuid: '222-222', + uuid: '555-555', }; const activeAlert2 = generateAlert({ id: 2, lastScheduledActionsGroup: 'default', - throttledActions: { '222-222': { date: '1969-12-31T23:10:00.000Z' } }, + throttledActions: { '555-555': { date: '1969-12-31T23:10:00.000Z' } }, }); const alertsWithOngoingAlert = { ...newAlert1, ...activeAlert2 }; @@ -431,10 +508,7 @@ describe('Per-Alert Action Scheduler', () => { rule: { ...rule, actions: [rule.actions[0], onThrottleIntervalAction] }, }); - const executables = await scheduler.generateExecutables({ - alerts: alertsWithOngoingAlert, - throttledSummaryActions: {}, - }); + const results = await scheduler.getActionsToSchedule({ alerts: alertsWithOngoingAlert }); expect(alertsClient.getSummarizedAlerts).not.toHaveBeenCalled(); expect(logger.debug).toHaveBeenCalledTimes(1); @@ -442,21 +516,28 @@ describe('Per-Alert Action Scheduler', () => { 1, `skipping scheduling of actions for '2' in rule rule-label: rule is throttled` ); - expect(executables).toHaveLength(3); - // @ts-expect-error private variable - expect(scheduler.skippedAlerts).toEqual({ '2': { reason: 'throttled' } }); + expect(ruleRunMetricsStore.getNumberOfGeneratedActions()).toEqual(3); + expect(ruleRunMetricsStore.getNumberOfTriggeredActions()).toEqual(3); + expect(ruleRunMetricsStore.getStatusByConnectorType('test')).toEqual({ + numberOfGeneratedActions: 3, + numberOfTriggeredActions: 3, + }); - expect(executables).toEqual([ - { action: rule.actions[0], alert: alertsWithOngoingAlert['1'] }, - { action: rule.actions[0], alert: alertsWithOngoingAlert['2'] }, - { action: onThrottleIntervalAction, alert: alertsWithOngoingAlert['1'] }, + expect(results).toHaveLength(3); + expect(results).toEqual([ + getResult('action-1', '1', '111-111'), + getResult('action-1', '2', '111-111'), + getResult('action-5', '1', '555-555'), ]); + + // @ts-expect-error private variable + expect(scheduler.skippedAlerts).toEqual({ '2': { reason: 'throttled' } }); }); - test('should not skip generating executable when throttle interval has passed and notifyWhen is onThrottleInterval', async () => { + test('should not skip creating actions to schedule when throttle interval has passed and notifyWhen is onThrottleInterval', async () => { const onThrottleIntervalAction: SanitizedRuleAction = { - id: '2', + id: 'action-5', group: 'default', actionTypeId: 'test', frequency: { summary: false, notifyWhen: 'onThrottleInterval', throttle: '1h' }, @@ -467,7 +548,7 @@ describe('Per-Alert Action Scheduler', () => { alertVal: 'My {{rule.id}} {{rule.name}} {{rule.spaceId}} {{rule.tags}} {{alert.id}} goes here', }, - uuid: '222-222', + uuid: '555-555', }; const activeAlert2 = generateAlert({ @@ -482,24 +563,28 @@ describe('Per-Alert Action Scheduler', () => { rule: { ...rule, actions: [rule.actions[0], onThrottleIntervalAction] }, }); - const executables = await scheduler.generateExecutables({ - alerts: alertsWithOngoingAlert, - throttledSummaryActions: {}, - }); + const results = await scheduler.getActionsToSchedule({ alerts: alertsWithOngoingAlert }); expect(alertsClient.getSummarizedAlerts).not.toHaveBeenCalled(); expect(logger.debug).not.toHaveBeenCalled(); - expect(executables).toHaveLength(4); - // @ts-expect-error private variable - expect(scheduler.skippedAlerts).toEqual({}); + expect(ruleRunMetricsStore.getNumberOfGeneratedActions()).toEqual(4); + expect(ruleRunMetricsStore.getNumberOfTriggeredActions()).toEqual(4); + expect(ruleRunMetricsStore.getStatusByConnectorType('test')).toEqual({ + numberOfGeneratedActions: 4, + numberOfTriggeredActions: 4, + }); - expect(executables).toEqual([ - { action: rule.actions[0], alert: alertsWithOngoingAlert['1'] }, - { action: rule.actions[0], alert: alertsWithOngoingAlert['2'] }, - { action: onThrottleIntervalAction, alert: alertsWithOngoingAlert['1'] }, - { action: onThrottleIntervalAction, alert: alertsWithOngoingAlert['2'] }, + expect(results).toHaveLength(4); + expect(results).toEqual([ + getResult('action-1', '1', '111-111'), + getResult('action-1', '2', '111-111'), + getResult('action-5', '1', '555-555'), + getResult('action-5', '2', '555-555'), ]); + + // @ts-expect-error private variable + expect(scheduler.skippedAlerts).toEqual({}); }); test('should query for summarized alerts if useAlertDataForTemplate is true', async () => { @@ -517,7 +602,7 @@ describe('Per-Alert Action Scheduler', () => { }; alertsClient.getSummarizedAlerts.mockResolvedValue(summarizedAlerts); const actionWithUseAlertDataForTemplate: SanitizedRuleAction = { - id: '1', + id: 'action-6', group: 'default', actionTypeId: 'test', frequency: { summary: false, notifyWhen: 'onActiveAlert', throttle: null }, @@ -528,33 +613,36 @@ describe('Per-Alert Action Scheduler', () => { alertVal: 'My {{rule.id}} {{rule.name}} {{rule.spaceId}} {{rule.tags}} {{alert.id}} goes here', }, - uuid: '111-111', + uuid: '666-666', useAlertDataForTemplate: true, }; const scheduler = new PerAlertActionScheduler({ ...getSchedulerContext(), rule: { ...rule, actions: [rule.actions[0], actionWithUseAlertDataForTemplate] }, }); - const executables = await scheduler.generateExecutables({ - alerts, - throttledSummaryActions: {}, - }); + const results = await scheduler.getActionsToSchedule({ alerts }); expect(alertsClient.getSummarizedAlerts).toHaveBeenCalledTimes(1); expect(alertsClient.getSummarizedAlerts).toHaveBeenCalledWith({ excludedAlertInstanceIds: [], executionUuid: defaultSchedulerContext.executionId, - ruleId: '1', + ruleId: 'rule-id-1', spaceId: 'test1', }); - expect(executables).toHaveLength(4); + expect(ruleRunMetricsStore.getNumberOfGeneratedActions()).toEqual(4); + expect(ruleRunMetricsStore.getNumberOfTriggeredActions()).toEqual(4); + expect(ruleRunMetricsStore.getStatusByConnectorType('test')).toEqual({ + numberOfGeneratedActions: 4, + numberOfTriggeredActions: 4, + }); - expect(executables).toEqual([ - { action: rule.actions[0], alert: alerts['1'] }, - { action: rule.actions[0], alert: alerts['2'] }, - { action: actionWithUseAlertDataForTemplate, alert: alerts['1'] }, - { action: actionWithUseAlertDataForTemplate, alert: alerts['2'] }, + expect(results).toHaveLength(4); + expect(results).toEqual([ + getResult('action-1', '1', '111-111'), + getResult('action-1', '2', '111-111'), + getResult('action-6', '1', '666-666'), + getResult('action-6', '2', '666-666'), ]); }); @@ -573,7 +661,7 @@ describe('Per-Alert Action Scheduler', () => { }; alertsClient.getSummarizedAlerts.mockResolvedValue(summarizedAlerts); const actionWithUseAlertDataForTemplate: SanitizedRuleAction = { - id: '1', + id: 'action-6', group: 'default', actionTypeId: 'test', frequency: { summary: false, notifyWhen: 'onThrottleInterval', throttle: '1h' }, @@ -584,34 +672,37 @@ describe('Per-Alert Action Scheduler', () => { alertVal: 'My {{rule.id}} {{rule.name}} {{rule.spaceId}} {{rule.tags}} {{alert.id}} goes here', }, - uuid: '111-111', + uuid: '666-666', useAlertDataForTemplate: true, }; const scheduler = new PerAlertActionScheduler({ ...getSchedulerContext(), rule: { ...rule, actions: [rule.actions[0], actionWithUseAlertDataForTemplate] }, }); - const executables = await scheduler.generateExecutables({ - alerts, - throttledSummaryActions: {}, - }); + const results = await scheduler.getActionsToSchedule({ alerts }); expect(alertsClient.getSummarizedAlerts).toHaveBeenCalledTimes(1); expect(alertsClient.getSummarizedAlerts).toHaveBeenCalledWith({ excludedAlertInstanceIds: [], - ruleId: '1', + ruleId: 'rule-id-1', spaceId: 'test1', start: new Date('1969-12-31T23:00:00.000Z'), end: new Date('1970-01-01T00:00:00.000Z'), }); - expect(executables).toHaveLength(4); + expect(ruleRunMetricsStore.getNumberOfGeneratedActions()).toEqual(4); + expect(ruleRunMetricsStore.getNumberOfTriggeredActions()).toEqual(4); + expect(ruleRunMetricsStore.getStatusByConnectorType('test')).toEqual({ + numberOfGeneratedActions: 4, + numberOfTriggeredActions: 4, + }); - expect(executables).toEqual([ - { action: rule.actions[0], alert: alerts['1'] }, - { action: rule.actions[0], alert: alerts['2'] }, - { action: actionWithUseAlertDataForTemplate, alert: alerts['1'] }, - { action: actionWithUseAlertDataForTemplate, alert: alerts['2'] }, + expect(results).toHaveLength(4); + expect(results).toEqual([ + getResult('action-1', '1', '111-111'), + getResult('action-1', '2', '111-111'), + getResult('action-6', '1', '666-666'), + getResult('action-6', '2', '666-666'), ]); }); @@ -630,7 +721,7 @@ describe('Per-Alert Action Scheduler', () => { }; alertsClient.getSummarizedAlerts.mockResolvedValue(summarizedAlerts); const actionWithAlertsFilter: SanitizedRuleAction = { - id: '1', + id: 'action-7', group: 'default', actionTypeId: 'test', frequency: { summary: false, notifyWhen: 'onActiveAlert', throttle: null }, @@ -641,34 +732,37 @@ describe('Per-Alert Action Scheduler', () => { alertVal: 'My {{rule.id}} {{rule.name}} {{rule.spaceId}} {{rule.tags}} {{alert.id}} goes here', }, - uuid: '111-111', + uuid: '777-777', alertsFilter: { query: { kql: 'kibana.alert.rule.name:foo', filters: [] } }, }; const scheduler = new PerAlertActionScheduler({ ...getSchedulerContext(), rule: { ...rule, actions: [rule.actions[0], actionWithAlertsFilter] }, }); - const executables = await scheduler.generateExecutables({ - alerts, - throttledSummaryActions: {}, - }); + const results = await scheduler.getActionsToSchedule({ alerts }); expect(alertsClient.getSummarizedAlerts).toHaveBeenCalledTimes(1); expect(alertsClient.getSummarizedAlerts).toHaveBeenCalledWith({ excludedAlertInstanceIds: [], executionUuid: defaultSchedulerContext.executionId, - ruleId: '1', + ruleId: 'rule-id-1', spaceId: 'test1', alertsFilter: { query: { kql: 'kibana.alert.rule.name:foo', filters: [] } }, }); - expect(executables).toHaveLength(4); + expect(ruleRunMetricsStore.getNumberOfGeneratedActions()).toEqual(4); + expect(ruleRunMetricsStore.getNumberOfTriggeredActions()).toEqual(4); + expect(ruleRunMetricsStore.getStatusByConnectorType('test')).toEqual({ + numberOfGeneratedActions: 4, + numberOfTriggeredActions: 4, + }); - expect(executables).toEqual([ - { action: rule.actions[0], alert: alerts['1'] }, - { action: rule.actions[0], alert: alerts['2'] }, - { action: actionWithAlertsFilter, alert: alerts['1'] }, - { action: actionWithAlertsFilter, alert: alerts['2'] }, + expect(results).toHaveLength(4); + expect(results).toEqual([ + getResult('action-1', '1', '111-111'), + getResult('action-1', '2', '111-111'), + getResult('action-7', '1', '777-777'), + getResult('action-7', '2', '777-777'), ]); }); @@ -687,7 +781,7 @@ describe('Per-Alert Action Scheduler', () => { }; alertsClient.getSummarizedAlerts.mockResolvedValue(summarizedAlerts); const actionWithAlertsFilter: SanitizedRuleAction = { - id: '1', + id: 'action-7', group: 'default', actionTypeId: 'test', frequency: { summary: false, notifyWhen: 'onThrottleInterval', throttle: '6h' }, @@ -698,39 +792,42 @@ describe('Per-Alert Action Scheduler', () => { alertVal: 'My {{rule.id}} {{rule.name}} {{rule.spaceId}} {{rule.tags}} {{alert.id}} goes here', }, - uuid: '111-111', + uuid: '777-777', alertsFilter: { query: { kql: 'kibana.alert.rule.name:foo', filters: [] } }, }; const scheduler = new PerAlertActionScheduler({ ...getSchedulerContext(), rule: { ...rule, actions: [rule.actions[0], actionWithAlertsFilter] }, }); - const executables = await scheduler.generateExecutables({ - alerts, - throttledSummaryActions: {}, - }); + const results = await scheduler.getActionsToSchedule({ alerts }); expect(alertsClient.getSummarizedAlerts).toHaveBeenCalledTimes(1); expect(alertsClient.getSummarizedAlerts).toHaveBeenCalledWith({ excludedAlertInstanceIds: [], - ruleId: '1', + ruleId: 'rule-id-1', spaceId: 'test1', alertsFilter: { query: { kql: 'kibana.alert.rule.name:foo', filters: [] } }, start: new Date('1969-12-31T18:00:00.000Z'), end: new Date('1970-01-01T00:00:00.000Z'), }); - expect(executables).toHaveLength(4); + expect(ruleRunMetricsStore.getNumberOfGeneratedActions()).toEqual(4); + expect(ruleRunMetricsStore.getNumberOfTriggeredActions()).toEqual(4); + expect(ruleRunMetricsStore.getStatusByConnectorType('test')).toEqual({ + numberOfGeneratedActions: 4, + numberOfTriggeredActions: 4, + }); - expect(executables).toEqual([ - { action: rule.actions[0], alert: alerts['1'] }, - { action: rule.actions[0], alert: alerts['2'] }, - { action: actionWithAlertsFilter, alert: alerts['1'] }, - { action: actionWithAlertsFilter, alert: alerts['2'] }, + expect(results).toHaveLength(4); + expect(results).toEqual([ + getResult('action-1', '1', '111-111'), + getResult('action-1', '2', '111-111'), + getResult('action-7', '1', '777-777'), + getResult('action-7', '2', '777-777'), ]); }); - test('should skip generating executable if alert does not match any alerts in summarized alerts', async () => { + test('should skip creating actions to schedule if alert does not match any alerts in summarized alerts', async () => { alertsClient.getProcessedAlerts.mockReturnValue(alerts); const summarizedAlerts = { new: { @@ -745,7 +842,7 @@ describe('Per-Alert Action Scheduler', () => { }; alertsClient.getSummarizedAlerts.mockResolvedValue(summarizedAlerts); const actionWithAlertsFilter: SanitizedRuleAction = { - id: '1', + id: 'action-8', group: 'default', actionTypeId: 'test', frequency: { summary: false, notifyWhen: 'onActiveAlert', throttle: null }, @@ -756,33 +853,36 @@ describe('Per-Alert Action Scheduler', () => { alertVal: 'My {{rule.id}} {{rule.name}} {{rule.spaceId}} {{rule.tags}} {{alert.id}} goes here', }, - uuid: '111-111', + uuid: '888-888', alertsFilter: { query: { kql: 'kibana.alert.rule.name:foo', filters: [] } }, }; const scheduler = new PerAlertActionScheduler({ ...getSchedulerContext(), rule: { ...rule, actions: [rule.actions[0], actionWithAlertsFilter] }, }); - const executables = await scheduler.generateExecutables({ - alerts, - throttledSummaryActions: {}, - }); + const results = await scheduler.getActionsToSchedule({ alerts }); expect(alertsClient.getSummarizedAlerts).toHaveBeenCalledTimes(1); expect(alertsClient.getSummarizedAlerts).toHaveBeenCalledWith({ excludedAlertInstanceIds: [], executionUuid: defaultSchedulerContext.executionId, - ruleId: '1', + ruleId: 'rule-id-1', spaceId: 'test1', alertsFilter: { query: { kql: 'kibana.alert.rule.name:foo', filters: [] } }, }); - expect(executables).toHaveLength(3); + expect(ruleRunMetricsStore.getNumberOfGeneratedActions()).toEqual(3); + expect(ruleRunMetricsStore.getNumberOfTriggeredActions()).toEqual(3); + expect(ruleRunMetricsStore.getStatusByConnectorType('test')).toEqual({ + numberOfGeneratedActions: 3, + numberOfTriggeredActions: 3, + }); - expect(executables).toEqual([ - { action: rule.actions[0], alert: alerts['1'] }, - { action: rule.actions[0], alert: alerts['2'] }, - { action: actionWithAlertsFilter, alert: alerts['1'] }, + expect(results).toHaveLength(3); + expect(results).toEqual([ + getResult('action-1', '1', '111-111'), + getResult('action-1', '2', '111-111'), + getResult('action-8', '1', '888-888'), ]); }); @@ -801,7 +901,7 @@ describe('Per-Alert Action Scheduler', () => { }; alertsClient.getSummarizedAlerts.mockResolvedValue(summarizedAlerts); const actionWithAlertsFilter: SanitizedRuleAction = { - id: '1', + id: 'action-9', group: 'default', actionTypeId: 'test', frequency: { summary: false, notifyWhen: 'onActiveAlert', throttle: null }, @@ -812,38 +912,168 @@ describe('Per-Alert Action Scheduler', () => { alertVal: 'My {{rule.id}} {{rule.name}} {{rule.spaceId}} {{rule.tags}} {{alert.id}} goes here', }, - uuid: '111-111', + uuid: '999-999', alertsFilter: { query: { kql: 'kibana.alert.rule.name:foo', filters: [] } }, }; const scheduler = new PerAlertActionScheduler({ ...getSchedulerContext(), rule: { ...rule, actions: [rule.actions[0], actionWithAlertsFilter] }, }); - const executables = await scheduler.generateExecutables({ - alerts, - throttledSummaryActions: {}, - }); + const results = await scheduler.getActionsToSchedule({ alerts }); expect(alertsClient.getSummarizedAlerts).toHaveBeenCalledTimes(1); expect(alertsClient.getSummarizedAlerts).toHaveBeenCalledWith({ excludedAlertInstanceIds: [], executionUuid: defaultSchedulerContext.executionId, - ruleId: '1', + ruleId: 'rule-id-1', spaceId: 'test1', alertsFilter: { query: { kql: 'kibana.alert.rule.name:foo', filters: [] } }, }); - expect(executables).toHaveLength(4); + expect(ruleRunMetricsStore.getNumberOfGeneratedActions()).toEqual(4); + expect(ruleRunMetricsStore.getNumberOfTriggeredActions()).toEqual(4); + expect(ruleRunMetricsStore.getStatusByConnectorType('test')).toEqual({ + numberOfGeneratedActions: 4, + numberOfTriggeredActions: 4, + }); + + expect(results).toHaveLength(4); + expect(results).toEqual([ + getResult('action-1', '1', '111-111'), + getResult('action-1', '2', '111-111'), + getResult('action-9', '1', '999-999'), + getResult('action-9', '2', '999-999'), + ]); expect(alerts['1'].getAlertAsData()).not.toBeUndefined(); expect(alerts['2'].getAlertAsData()).not.toBeUndefined(); + }); + + test('should skip creating actions to schedule if overall max actions limit exceeded', async () => { + const defaultContext = getSchedulerContext(); + const scheduler = new PerAlertActionScheduler({ + ...defaultContext, + taskRunnerContext: { + ...defaultContext.taskRunnerContext, + actionsConfigMap: { + default: { max: 3 }, + }, + }, + }); + const results = await scheduler.getActionsToSchedule({ alerts }); + + expect(alertsClient.getSummarizedAlerts).not.toHaveBeenCalled(); - expect(executables).toEqual([ - { action: rule.actions[0], alert: alerts['1'] }, - { action: rule.actions[0], alert: alerts['2'] }, - { action: actionWithAlertsFilter, alert: alerts['1'] }, - { action: actionWithAlertsFilter, alert: alerts['2'] }, + expect(ruleRunMetricsStore.getNumberOfGeneratedActions()).toEqual(4); + expect(ruleRunMetricsStore.getNumberOfTriggeredActions()).toEqual(3); + expect(ruleRunMetricsStore.getStatusByConnectorType('test')).toEqual({ + numberOfGeneratedActions: 4, + numberOfTriggeredActions: 3, + triggeredActionsStatus: ActionsCompletion.PARTIAL, + }); + + expect(logger.debug).toHaveBeenCalledWith( + `Rule "rule-id-1" skipped scheduling action "action-2" because the maximum number of allowed actions has been reached.` + ); + + expect(results).toHaveLength(3); + expect(results).toEqual([ + getResult('action-1', '1', '111-111'), + getResult('action-1', '2', '111-111'), + getResult('action-2', '1', '222-222'), ]); }); + + test('should skip creating actions to schedule if connector type max actions limit exceeded', async () => { + const defaultContext = getSchedulerContext(); + const scheduler = new PerAlertActionScheduler({ + ...defaultContext, + taskRunnerContext: { + ...defaultContext.taskRunnerContext, + actionsConfigMap: { + default: { max: 1000 }, + test: { max: 1 }, + }, + }, + }); + const results = await scheduler.getActionsToSchedule({ alerts }); + + expect(alertsClient.getSummarizedAlerts).not.toHaveBeenCalled(); + + expect(ruleRunMetricsStore.getNumberOfGeneratedActions()).toEqual(4); + expect(ruleRunMetricsStore.getNumberOfTriggeredActions()).toEqual(1); + expect(ruleRunMetricsStore.getStatusByConnectorType('test')).toEqual({ + numberOfGeneratedActions: 4, + numberOfTriggeredActions: 1, + triggeredActionsStatus: ActionsCompletion.PARTIAL, + }); + + expect(logger.debug).toHaveBeenCalledWith( + `Rule "rule-id-1" skipped scheduling action "action-1" because the maximum number of allowed actions for connector type test has been reached.` + ); + + expect(results).toHaveLength(1); + expect(results).toEqual([getResult('action-1', '1', '111-111')]); + }); + + test('should correctly update last scheduled actions for alert when action is "onActiveAlert"', async () => { + const alert = new Alert('1', { + state: { test: true }, + meta: {}, + }); + alert.scheduleActions('default'); + const scheduler = new PerAlertActionScheduler({ + ...getSchedulerContext(), + rule: { ...rule, actions: [rule.actions[0]] }, + }); + + expect(alert.getLastScheduledActions()).toBeUndefined(); + expect(alert.hasScheduledActions()).toBe(true); + await scheduler.getActionsToSchedule({ alerts: { '1': alert } }); + + expect(alert.getLastScheduledActions()).toEqual({ + date: '1970-01-01T00:00:00.000Z', + group: 'default', + }); + expect(alert.hasScheduledActions()).toBe(false); + }); + + test('should correctly update last scheduled actions for alert', async () => { + const alert = new Alert('1', { + state: { test: true }, + meta: {}, + }); + alert.scheduleActions('default'); + const onThrottleIntervalAction: SanitizedRuleAction = { + id: 'action-4', + group: 'default', + actionTypeId: 'test', + frequency: { summary: false, notifyWhen: 'onThrottleInterval', throttle: '1h' }, + params: { + foo: true, + contextVal: 'My {{context.value}} goes here', + stateVal: 'My {{state.value}} goes here', + alertVal: + 'My {{rule.id}} {{rule.name}} {{rule.spaceId}} {{rule.tags}} {{alert.id}} goes here', + }, + uuid: '222-222', + }; + + expect(alert.getLastScheduledActions()).toBeUndefined(); + expect(alert.hasScheduledActions()).toBe(true); + const scheduler = new PerAlertActionScheduler({ + ...getSchedulerContext(), + rule: { ...rule, actions: [onThrottleIntervalAction] }, + }); + + await scheduler.getActionsToSchedule({ alerts: { '1': alert } }); + + expect(alert.getLastScheduledActions()).toEqual({ + date: '1970-01-01T00:00:00.000Z', + group: 'default', + actions: { '222-222': { date: '1970-01-01T00:00:00.000Z' } }, + }); + expect(alert.hasScheduledActions()).toBe(false); + }); }); }); diff --git a/x-pack/plugins/alerting/server/task_runner/action_scheduler/schedulers/per_alert_action_scheduler.ts b/x-pack/plugins/alerting/server/task_runner/action_scheduler/schedulers/per_alert_action_scheduler.ts index 602d3c31688c1..b35d86dff0105 100644 --- a/x-pack/plugins/alerting/server/task_runner/action_scheduler/schedulers/per_alert_action_scheduler.ts +++ b/x-pack/plugins/alerting/server/task_runner/action_scheduler/schedulers/per_alert_action_scheduler.ts @@ -12,19 +12,24 @@ import { RuleTypeState, RuleAlertData, parseDuration } from '../../../../common' import { GetSummarizedAlertsParams } from '../../../alerts_client/types'; import { AlertHit } from '../../../types'; import { Alert } from '../../../alert'; -import { getSummarizedAlerts } from '../get_summarized_alerts'; import { + buildRuleUrl, + formatActionToEnqueue, generateActionHash, + getSummarizedAlerts, isActionOnInterval, isSummaryAction, logNumberOfFilteredAlerts, -} from '../rule_action_helper'; + shouldScheduleAction, +} from '../lib'; import { ActionSchedulerOptions, - Executable, - GenerateExecutablesOpts, + ActionsToSchedule, + GetActionsToScheduleOpts, IActionScheduler, } from '../types'; +import { TransformActionParamsOptions, transformActionParams } from '../../transform_action_params'; +import { injectActionParams } from '../../inject_action_params'; enum Reasons { MUTED = 'muted', @@ -90,12 +95,16 @@ export class PerAlertActionScheduler< return 2; } - public async generateExecutables({ + public async getActionsToSchedule({ alerts, - }: GenerateExecutablesOpts): Promise< - Array> + }: GetActionsToScheduleOpts): Promise< + ActionsToSchedule[] > { - const executables = []; + const executables: Array<{ + action: RuleAction; + alert: Alert; + }> = []; + const results: ActionsToSchedule[] = []; const alertsArray = Object.entries(alerts); for (const action of this.actions) { @@ -104,7 +113,7 @@ export class PerAlertActionScheduler< if (action.useAlertDataForTemplate || action.alertsFilter) { const optionsBase = { spaceId: this.context.taskInstance.params.spaceId, - ruleId: this.context.taskInstance.params.alertId, + ruleId: this.context.rule.id, excludedAlertInstanceIds: this.context.rule.mutedInstanceIds, alertsFilter: action.alertsFilter, }; @@ -135,7 +144,7 @@ export class PerAlertActionScheduler< if (alertMaintenanceWindowIds.length !== 0) { this.context.logger.debug( `no scheduling of summary actions "${action.id}" for rule "${ - this.context.taskInstance.params.alertId + this.context.rule.id }": has active maintenance windows ${alertMaintenanceWindowIds.join(', ')}.` ); continue; @@ -185,7 +194,112 @@ export class PerAlertActionScheduler< } } - return executables; + if (executables.length === 0) return []; + + this.context.ruleRunMetricsStore.incrementNumberOfGeneratedActions(executables.length); + + const ruleUrl = buildRuleUrl({ + getViewInAppRelativeUrl: this.context.ruleType.getViewInAppRelativeUrl, + kibanaBaseUrl: this.context.taskRunnerContext.kibanaBaseUrl, + logger: this.context.logger, + rule: this.context.rule, + spaceId: this.context.taskInstance.params.spaceId, + }); + + for (const { action, alert } of executables) { + const { actionTypeId } = action; + + if ( + !shouldScheduleAction({ + action, + actionsConfigMap: this.context.taskRunnerContext.actionsConfigMap, + isActionExecutable: this.context.taskRunnerContext.actionsPlugin.isActionExecutable, + logger: this.context.logger, + ruleId: this.context.rule.id, + ruleRunMetricsStore: this.context.ruleRunMetricsStore, + }) + ) { + continue; + } + + this.context.ruleRunMetricsStore.incrementNumberOfTriggeredActions(); + this.context.ruleRunMetricsStore.incrementNumberOfTriggeredActionsByConnectorType( + actionTypeId + ); + + const actionGroup = action.group as ActionGroupIds; + const transformActionParamsOptions: TransformActionParamsOptions = { + actionsPlugin: this.context.taskRunnerContext.actionsPlugin, + alertId: this.context.rule.id, + alertType: this.context.ruleType.id, + actionTypeId: action.actionTypeId, + alertName: this.context.rule.name, + spaceId: this.context.taskInstance.params.spaceId, + tags: this.context.rule.tags, + alertInstanceId: alert.getId(), + alertUuid: alert.getUuid(), + alertActionGroup: actionGroup, + alertActionGroupName: this.ruleTypeActionGroups!.get(actionGroup)!, + context: alert.getContext(), + actionId: action.id, + state: alert.getState(), + kibanaBaseUrl: this.context.taskRunnerContext.kibanaBaseUrl, + alertParams: this.context.rule.params, + actionParams: action.params, + flapping: alert.getFlapping(), + ruleUrl: ruleUrl?.absoluteUrl, + }; + + if (alert.isAlertAsData()) { + transformActionParamsOptions.aadAlert = alert.getAlertAsData(); + } + + const actionToRun = { + ...action, + params: injectActionParams({ + actionTypeId: action.actionTypeId, + ruleUrl, + ruleName: this.context.rule.name, + actionParams: transformActionParams(transformActionParamsOptions), + }), + }; + + results.push({ + actionToEnqueue: formatActionToEnqueue({ + action: actionToRun, + apiKey: this.context.apiKey, + executionId: this.context.executionId, + ruleConsumer: this.context.ruleConsumer, + ruleId: this.context.rule.id, + ruleTypeId: this.context.ruleType.id, + spaceId: this.context.taskInstance.params.spaceId, + }), + actionToLog: { + id: action.id, + // uuid is typed as optional but in reality it is always + // populated - https://github.com/elastic/kibana/issues/195255 + uuid: action.uuid, + typeId: action.actionTypeId, + alertId: alert.getId(), + alertGroup: action.group, + }, + }); + + if (!this.isRecoveredAlert(actionGroup)) { + if (isActionOnInterval(action)) { + alert.updateLastScheduledActions( + action.group as ActionGroupIds, + generateActionHash(action), + action.uuid + ); + } else { + alert.updateLastScheduledActions(action.group as ActionGroupIds); + } + alert.unscheduleActions(); + } + } + + return results; } private isAlertMuted(alertId: string) { diff --git a/x-pack/plugins/alerting/server/task_runner/action_scheduler/schedulers/summary_action_scheduler.test.ts b/x-pack/plugins/alerting/server/task_runner/action_scheduler/schedulers/summary_action_scheduler.test.ts index 600dd0e1951d5..fc810fc4ef34c 100644 --- a/x-pack/plugins/alerting/server/task_runner/action_scheduler/schedulers/summary_action_scheduler.test.ts +++ b/x-pack/plugins/alerting/server/task_runner/action_scheduler/schedulers/summary_action_scheduler.test.ts @@ -20,6 +20,8 @@ import { getErrorSource, TaskErrorSource, } from '@kbn/task-manager-plugin/server/task_running/errors'; +import { CombinedSummarizedAlerts } from '../../../types'; +import { ActionsCompletion } from '@kbn/alerting-state-types'; const alertingEventLogger = alertingEventLoggerMock.create(); const actionsClient = actionsClientMock.create(); @@ -29,9 +31,10 @@ const logger = loggingSystemMock.create().get(); let ruleRunMetricsStore: RuleRunMetricsStore; const rule = getRule({ + id: 'rule-id-1', actions: [ { - id: '1', + id: 'action-1', group: 'default', actionTypeId: 'test', frequency: { summary: false, notifyWhen: 'onActiveAlert', throttle: null }, @@ -45,7 +48,7 @@ const rule = getRule({ uuid: '111-111', }, { - id: '2', + id: 'action-2', group: 'default', actionTypeId: 'test', frequency: { summary: true, notifyWhen: 'onActiveAlert', throttle: null }, @@ -59,7 +62,7 @@ const rule = getRule({ uuid: '222-222', }, { - id: '3', + id: 'action-3', group: 'default', actionTypeId: 'test', frequency: { summary: true, notifyWhen: 'onActiveAlert', throttle: null }, @@ -88,6 +91,30 @@ const getSchedulerContext = (params = {}) => { return { ...defaultSchedulerContext, rule, ...params, ruleRunMetricsStore }; }; +const getResult = (actionId: string, actionUuid: string, summary: CombinedSummarizedAlerts) => ({ + actionToEnqueue: { + actionTypeId: 'test', + apiKey: 'MTIzOmFiYw==', + consumer: 'rule-consumer', + executionId: '5f6aa57d-3e22-484e-bae8-cbed868f4d28', + id: actionId, + uuid: actionUuid, + relatedSavedObjects: [{ id: 'rule-id-1', namespace: 'test1', type: 'alert', typeId: 'test' }], + source: { source: { id: 'rule-id-1', type: 'alert' }, type: 'SAVED_OBJECT' }, + spaceId: 'test1', + }, + actionToLog: { + alertSummary: { + new: summary.new.count, + ongoing: summary.ongoing.count, + recovered: summary.recovered.count, + }, + id: actionId, + uuid: actionUuid, + typeId: 'test', + }, +}); + let clock: sinon.SinonFakeTimers; describe('Summary Action Scheduler', () => { @@ -127,21 +154,21 @@ describe('Summary Action Scheduler', () => { expect(logger.error).toHaveBeenCalledTimes(2); expect(logger.error).toHaveBeenNthCalledWith( 1, - `Skipping action \"2\" for rule \"1\" because the rule type \"Test\" does not support alert-as-data.` + `Skipping action \"action-2\" for rule \"rule-id-1\" because the rule type \"Test\" does not support alert-as-data.` ); expect(logger.error).toHaveBeenNthCalledWith( 2, - `Skipping action \"3\" for rule \"1\" because the rule type \"Test\" does not support alert-as-data.` + `Skipping action \"action-3\" for rule \"rule-id-1\" because the rule type \"Test\" does not support alert-as-data.` ); }); - describe('generateExecutables', () => { + describe('getActionsToSchedule', () => { const newAlert1 = generateAlert({ id: 1 }); const newAlert2 = generateAlert({ id: 2 }); const alerts = { ...newAlert1, ...newAlert2 }; const summaryActionWithAlertFilter: RuleAction = { - id: '2', + id: 'action-3', group: 'default', actionTypeId: 'test', frequency: { @@ -157,11 +184,11 @@ describe('Summary Action Scheduler', () => { 'My {{rule.id}} {{rule.name}} {{rule.spaceId}} {{rule.tags}} {{alert.id}} goes here', }, alertsFilter: { query: { kql: 'kibana.alert.rule.name:foo', dsl: '{}', filters: [] } }, - uuid: '222-222', + uuid: '333-333', }; const summaryActionWithThrottle: RuleAction = { - id: '2', + id: 'action-4', group: 'default', actionTypeId: 'test', frequency: { @@ -176,10 +203,10 @@ describe('Summary Action Scheduler', () => { alertVal: 'My {{rule.id}} {{rule.name}} {{rule.spaceId}} {{rule.tags}} {{alert.id}} goes here', }, - uuid: '222-222', + uuid: '444-444', }; - test('should generate executable for summary action when summary action is per rule run', async () => { + test('should create action to schedule for summary action when summary action is per rule run', async () => { alertsClient.getProcessedAlerts.mockReturnValue(alerts); const summarizedAlerts = { new: { count: 2, data: [mockAAD, mockAAD] }, @@ -188,37 +215,43 @@ describe('Summary Action Scheduler', () => { }; alertsClient.getSummarizedAlerts.mockResolvedValue(summarizedAlerts); + const throttledSummaryActions = {}; const scheduler = new SummaryActionScheduler(getSchedulerContext()); - const executables = await scheduler.generateExecutables({ - alerts, - throttledSummaryActions: {}, - }); + const results = await scheduler.getActionsToSchedule({ alerts, throttledSummaryActions }); + expect(throttledSummaryActions).toEqual({}); expect(alertsClient.getSummarizedAlerts).toHaveBeenCalledTimes(2); expect(alertsClient.getSummarizedAlerts).toHaveBeenNthCalledWith(1, { excludedAlertInstanceIds: [], executionUuid: defaultSchedulerContext.executionId, - ruleId: '1', + ruleId: 'rule-id-1', spaceId: 'test1', }); expect(alertsClient.getSummarizedAlerts).toHaveBeenNthCalledWith(2, { excludedAlertInstanceIds: [], executionUuid: defaultSchedulerContext.executionId, - ruleId: '1', + ruleId: 'rule-id-1', spaceId: 'test1', }); expect(logger.debug).not.toHaveBeenCalled(); - expect(executables).toHaveLength(2); + expect(ruleRunMetricsStore.getNumberOfGeneratedActions()).toEqual(2); + expect(ruleRunMetricsStore.getNumberOfTriggeredActions()).toEqual(2); + expect(ruleRunMetricsStore.getStatusByConnectorType('test')).toEqual({ + numberOfGeneratedActions: 2, + numberOfTriggeredActions: 2, + }); + + expect(results).toHaveLength(2); const finalSummary = { ...summarizedAlerts, all: { count: 2, data: [mockAAD, mockAAD] } }; - expect(executables).toEqual([ - { action: rule.actions[1], summarizedAlerts: finalSummary }, - { action: rule.actions[2], summarizedAlerts: finalSummary }, + expect(results).toEqual([ + getResult('action-2', '222-222', finalSummary), + getResult('action-3', '333-333', finalSummary), ]); }); - test('should generate executable for summary action when summary action has alertsFilter', async () => { + test('should create actions to schedule for summary action when summary action has alertsFilter', async () => { alertsClient.getProcessedAlerts.mockReturnValue(alerts); const summarizedAlerts = { new: { count: 2, data: [mockAAD, mockAAD] }, @@ -232,30 +265,34 @@ describe('Summary Action Scheduler', () => { rule: { ...rule, actions: [summaryActionWithAlertFilter] }, }); - const executables = await scheduler.generateExecutables({ - alerts, - throttledSummaryActions: {}, - }); + const throttledSummaryActions = {}; + const results = await scheduler.getActionsToSchedule({ alerts, throttledSummaryActions }); + expect(throttledSummaryActions).toEqual({}); expect(alertsClient.getSummarizedAlerts).toHaveBeenCalledTimes(1); expect(alertsClient.getSummarizedAlerts).toHaveBeenCalledWith({ excludedAlertInstanceIds: [], executionUuid: defaultSchedulerContext.executionId, - ruleId: '1', + ruleId: 'rule-id-1', spaceId: 'test1', alertsFilter: { query: { kql: 'kibana.alert.rule.name:foo', dsl: '{}', filters: [] } }, }); expect(logger.debug).not.toHaveBeenCalled(); - expect(executables).toHaveLength(1); + expect(ruleRunMetricsStore.getNumberOfGeneratedActions()).toEqual(1); + expect(ruleRunMetricsStore.getNumberOfTriggeredActions()).toEqual(1); + expect(ruleRunMetricsStore.getStatusByConnectorType('test')).toEqual({ + numberOfGeneratedActions: 1, + numberOfTriggeredActions: 1, + }); + + expect(results).toHaveLength(1); const finalSummary = { ...summarizedAlerts, all: { count: 2, data: [mockAAD, mockAAD] } }; - expect(executables).toEqual([ - { action: summaryActionWithAlertFilter, summarizedAlerts: finalSummary }, - ]); + expect(results).toEqual([getResult('action-3', '333-333', finalSummary)]); }); - test('should generate executable for summary action when summary action is throttled with no throttle history', async () => { + test('should create actions to schedule for summary action when summary action is throttled with no throttle history', async () => { alertsClient.getProcessedAlerts.mockReturnValue(alerts); const summarizedAlerts = { new: { count: 2, data: [mockAAD, mockAAD] }, @@ -269,48 +306,52 @@ describe('Summary Action Scheduler', () => { rule: { ...rule, actions: [summaryActionWithThrottle] }, }); - const executables = await scheduler.generateExecutables({ - alerts, - throttledSummaryActions: {}, - }); + const throttledSummaryActions = {}; + const results = await scheduler.getActionsToSchedule({ alerts, throttledSummaryActions }); + expect(throttledSummaryActions).toEqual({ '444-444': { date: '1970-01-01T00:00:00.000Z' } }); expect(alertsClient.getSummarizedAlerts).toHaveBeenCalledTimes(1); expect(alertsClient.getSummarizedAlerts).toHaveBeenCalledWith({ excludedAlertInstanceIds: [], - ruleId: '1', + ruleId: 'rule-id-1', spaceId: 'test1', start: new Date('1969-12-31T00:00:00.000Z'), end: new Date(), }); expect(logger.debug).not.toHaveBeenCalled(); - expect(executables).toHaveLength(1); + expect(ruleRunMetricsStore.getNumberOfGeneratedActions()).toEqual(1); + expect(ruleRunMetricsStore.getNumberOfTriggeredActions()).toEqual(1); + expect(ruleRunMetricsStore.getStatusByConnectorType('test')).toEqual({ + numberOfGeneratedActions: 1, + numberOfTriggeredActions: 1, + }); + + expect(results).toHaveLength(1); const finalSummary = { ...summarizedAlerts, all: { count: 2, data: [mockAAD, mockAAD] } }; - expect(executables).toEqual([ - { action: summaryActionWithThrottle, summarizedAlerts: finalSummary }, - ]); + expect(results).toEqual([getResult('action-4', '444-444', finalSummary)]); }); - test('should skip generating executable for summary action when summary action is throttled', async () => { + test('should skip creating actions to schedule for summary action when summary action is throttled', async () => { const scheduler = new SummaryActionScheduler({ ...getSchedulerContext(), rule: { ...rule, actions: [summaryActionWithThrottle] }, }); - const executables = await scheduler.generateExecutables({ - alerts, - throttledSummaryActions: { - '222-222': { date: '1969-12-31T13:00:00.000Z' }, - }, - }); + const throttledSummaryActions = { '444-444': { date: '1969-12-31T13:00:00.000Z' } }; + const results = await scheduler.getActionsToSchedule({ alerts, throttledSummaryActions }); + expect(throttledSummaryActions).toEqual({ '444-444': { date: '1969-12-31T13:00:00.000Z' } }); expect(alertsClient.getSummarizedAlerts).not.toHaveBeenCalled(); expect(logger.debug).toHaveBeenCalledWith( - `skipping scheduling the action 'test:2', summary action is still being throttled` + `skipping scheduling the action 'test:action-4', summary action is still being throttled` ); - expect(executables).toHaveLength(0); + expect(ruleRunMetricsStore.getNumberOfGeneratedActions()).toEqual(0); + expect(ruleRunMetricsStore.getNumberOfTriggeredActions()).toEqual(0); + + expect(results).toHaveLength(0); }); test('should remove new alerts from summary if suppressed by maintenance window', async () => { @@ -332,22 +373,21 @@ describe('Summary Action Scheduler', () => { alertsClient.getSummarizedAlerts.mockResolvedValue(summarizedAlerts); const scheduler = new SummaryActionScheduler(getSchedulerContext()); - const executables = await scheduler.generateExecutables({ - alerts, - throttledSummaryActions: {}, - }); + const throttledSummaryActions = {}; + const results = await scheduler.getActionsToSchedule({ alerts, throttledSummaryActions }); + expect(throttledSummaryActions).toEqual({}); expect(alertsClient.getSummarizedAlerts).toHaveBeenCalledTimes(2); expect(alertsClient.getSummarizedAlerts).toHaveBeenNthCalledWith(1, { excludedAlertInstanceIds: [], executionUuid: defaultSchedulerContext.executionId, - ruleId: '1', + ruleId: 'rule-id-1', spaceId: 'test1', }); expect(alertsClient.getSummarizedAlerts).toHaveBeenNthCalledWith(2, { excludedAlertInstanceIds: [], executionUuid: defaultSchedulerContext.executionId, - ruleId: '1', + ruleId: 'rule-id-1', spaceId: 'test1', }); expect(logger.debug).toHaveBeenCalledTimes(2); @@ -360,7 +400,14 @@ describe('Summary Action Scheduler', () => { `(1) alert has been filtered out for: test:333-333` ); - expect(executables).toHaveLength(2); + expect(ruleRunMetricsStore.getNumberOfGeneratedActions()).toEqual(2); + expect(ruleRunMetricsStore.getNumberOfTriggeredActions()).toEqual(2); + expect(ruleRunMetricsStore.getStatusByConnectorType('test')).toEqual({ + numberOfGeneratedActions: 2, + numberOfTriggeredActions: 2, + }); + + expect(results).toHaveLength(2); const finalSummary = { all: { count: 1, data: [newAADAlerts[1]] }, @@ -368,13 +415,13 @@ describe('Summary Action Scheduler', () => { ongoing: { count: 0, data: [] }, recovered: { count: 0, data: [] }, }; - expect(executables).toEqual([ - { action: rule.actions[1], summarizedAlerts: finalSummary }, - { action: rule.actions[2], summarizedAlerts: finalSummary }, + expect(results).toEqual([ + getResult('action-2', '222-222', finalSummary), + getResult('action-3', '333-333', finalSummary), ]); }); - test('should generate executable for summary action and log when alerts have been filtered out by action condition', async () => { + test('should create alerts to schedule for summary action and log when alerts have been filtered out by action condition', async () => { alertsClient.getProcessedAlerts.mockReturnValue(alerts); const summarizedAlerts = { new: { count: 1, data: [mockAAD] }, @@ -388,33 +435,37 @@ describe('Summary Action Scheduler', () => { rule: { ...rule, actions: [summaryActionWithAlertFilter] }, }); - const executables = await scheduler.generateExecutables({ - alerts, - throttledSummaryActions: {}, - }); + const throttledSummaryActions = {}; + const results = await scheduler.getActionsToSchedule({ alerts, throttledSummaryActions }); + expect(throttledSummaryActions).toEqual({}); expect(alertsClient.getSummarizedAlerts).toHaveBeenCalledTimes(1); expect(alertsClient.getSummarizedAlerts).toHaveBeenCalledWith({ excludedAlertInstanceIds: [], executionUuid: defaultSchedulerContext.executionId, - ruleId: '1', + ruleId: 'rule-id-1', spaceId: 'test1', alertsFilter: { query: { kql: 'kibana.alert.rule.name:foo', dsl: '{}', filters: [] } }, }); expect(logger.debug).toHaveBeenCalledTimes(1); expect(logger.debug).toHaveBeenCalledWith( - `(1) alert has been filtered out for: test:222-222` + `(1) alert has been filtered out for: test:333-333` ); - expect(executables).toHaveLength(1); + expect(ruleRunMetricsStore.getNumberOfGeneratedActions()).toEqual(1); + expect(ruleRunMetricsStore.getNumberOfTriggeredActions()).toEqual(1); + expect(ruleRunMetricsStore.getStatusByConnectorType('test')).toEqual({ + numberOfGeneratedActions: 1, + numberOfTriggeredActions: 1, + }); + + expect(results).toHaveLength(1); const finalSummary = { ...summarizedAlerts, all: { count: 1, data: [mockAAD] } }; - expect(executables).toEqual([ - { action: summaryActionWithAlertFilter, summarizedAlerts: finalSummary }, - ]); + expect(results).toEqual([getResult('action-3', '333-333', finalSummary)]); }); - test('should skip generating executable for summary action when no alerts found', async () => { + test('should skip creating actions to schedule for summary action when no alerts found', async () => { alertsClient.getProcessedAlerts.mockReturnValue(alerts); const summarizedAlerts = { new: { count: 0, data: [] }, @@ -428,22 +479,23 @@ describe('Summary Action Scheduler', () => { rule: { ...rule, actions: [summaryActionWithThrottle] }, }); - const executables = await scheduler.generateExecutables({ - alerts, - throttledSummaryActions: {}, - }); + const throttledSummaryActions = {}; + const results = await scheduler.getActionsToSchedule({ alerts, throttledSummaryActions }); + expect(throttledSummaryActions).toEqual({}); expect(alertsClient.getSummarizedAlerts).toHaveBeenCalledTimes(1); expect(alertsClient.getSummarizedAlerts).toHaveBeenCalledWith({ excludedAlertInstanceIds: [], - ruleId: '1', + ruleId: 'rule-id-1', spaceId: 'test1', start: new Date('1969-12-31T00:00:00.000Z'), end: new Date(), }); expect(logger.debug).not.toHaveBeenCalled(); - expect(executables).toHaveLength(0); + expect(ruleRunMetricsStore.getNumberOfGeneratedActions()).toEqual(0); + expect(ruleRunMetricsStore.getNumberOfTriggeredActions()).toEqual(0); + expect(results).toHaveLength(0); }); test('should throw framework error if getSummarizedAlerts throws error', async () => { @@ -455,14 +507,117 @@ describe('Summary Action Scheduler', () => { const scheduler = new SummaryActionScheduler(getSchedulerContext()); try { - await scheduler.generateExecutables({ - alerts, - throttledSummaryActions: {}, - }); + await scheduler.getActionsToSchedule({ alerts, throttledSummaryActions: {} }); } catch (err) { expect(err.message).toEqual(`no alerts for you`); expect(getErrorSource(err)).toBe(TaskErrorSource.FRAMEWORK); } }); + + test('should skip creating actions to schedule if overall max actions limit exceeded', async () => { + alertsClient.getProcessedAlerts.mockReturnValue(alerts); + const summarizedAlerts = { + new: { count: 2, data: [mockAAD, mockAAD] }, + ongoing: { count: 0, data: [] }, + recovered: { count: 0, data: [] }, + }; + alertsClient.getSummarizedAlerts.mockResolvedValue(summarizedAlerts); + + const defaultContext = getSchedulerContext(); + const scheduler = new SummaryActionScheduler({ + ...defaultContext, + taskRunnerContext: { + ...defaultContext.taskRunnerContext, + actionsConfigMap: { + default: { max: 1 }, + }, + }, + }); + const results = await scheduler.getActionsToSchedule({ alerts, throttledSummaryActions: {} }); + + expect(alertsClient.getSummarizedAlerts).toHaveBeenCalledTimes(2); + expect(alertsClient.getSummarizedAlerts).toHaveBeenNthCalledWith(1, { + excludedAlertInstanceIds: [], + executionUuid: defaultSchedulerContext.executionId, + ruleId: 'rule-id-1', + spaceId: 'test1', + }); + expect(alertsClient.getSummarizedAlerts).toHaveBeenNthCalledWith(2, { + excludedAlertInstanceIds: [], + executionUuid: defaultSchedulerContext.executionId, + ruleId: 'rule-id-1', + spaceId: 'test1', + }); + + expect(ruleRunMetricsStore.getNumberOfGeneratedActions()).toEqual(2); + expect(ruleRunMetricsStore.getNumberOfTriggeredActions()).toEqual(1); + expect(ruleRunMetricsStore.getStatusByConnectorType('test')).toEqual({ + numberOfGeneratedActions: 2, + numberOfTriggeredActions: 1, + triggeredActionsStatus: ActionsCompletion.PARTIAL, + }); + + expect(logger.debug).toHaveBeenCalledWith( + `Rule "rule-id-1" skipped scheduling action "action-3" because the maximum number of allowed actions has been reached.` + ); + + expect(results).toHaveLength(1); + + const finalSummary = { ...summarizedAlerts, all: { count: 2, data: [mockAAD, mockAAD] } }; + expect(results).toEqual([getResult('action-2', '222-222', finalSummary)]); + }); + + test('should skip creating actions to schedule if connector type max actions limit exceeded', async () => { + alertsClient.getProcessedAlerts.mockReturnValue(alerts); + const summarizedAlerts = { + new: { count: 2, data: [mockAAD, mockAAD] }, + ongoing: { count: 0, data: [] }, + recovered: { count: 0, data: [] }, + }; + alertsClient.getSummarizedAlerts.mockResolvedValue(summarizedAlerts); + + const defaultContext = getSchedulerContext(); + const scheduler = new SummaryActionScheduler({ + ...defaultContext, + taskRunnerContext: { + ...defaultContext.taskRunnerContext, + actionsConfigMap: { + default: { max: 1000 }, + test: { max: 1 }, + }, + }, + }); + const results = await scheduler.getActionsToSchedule({ alerts, throttledSummaryActions: {} }); + + expect(alertsClient.getSummarizedAlerts).toHaveBeenCalledTimes(2); + expect(alertsClient.getSummarizedAlerts).toHaveBeenNthCalledWith(1, { + excludedAlertInstanceIds: [], + executionUuid: defaultSchedulerContext.executionId, + ruleId: 'rule-id-1', + spaceId: 'test1', + }); + expect(alertsClient.getSummarizedAlerts).toHaveBeenNthCalledWith(2, { + excludedAlertInstanceIds: [], + executionUuid: defaultSchedulerContext.executionId, + ruleId: 'rule-id-1', + spaceId: 'test1', + }); + + expect(ruleRunMetricsStore.getNumberOfGeneratedActions()).toEqual(2); + expect(ruleRunMetricsStore.getNumberOfTriggeredActions()).toEqual(1); + expect(ruleRunMetricsStore.getStatusByConnectorType('test')).toEqual({ + numberOfGeneratedActions: 2, + numberOfTriggeredActions: 1, + triggeredActionsStatus: ActionsCompletion.PARTIAL, + }); + + expect(logger.debug).toHaveBeenCalledWith( + `Rule "rule-id-1" skipped scheduling action "action-3" because the maximum number of allowed actions for connector type test has been reached.` + ); + + expect(results).toHaveLength(1); + const finalSummary = { ...summarizedAlerts, all: { count: 2, data: [mockAAD, mockAAD] } }; + expect(results).toEqual([getResult('action-2', '222-222', finalSummary)]); + }); }); }); diff --git a/x-pack/plugins/alerting/server/task_runner/action_scheduler/schedulers/summary_action_scheduler.ts b/x-pack/plugins/alerting/server/task_runner/action_scheduler/schedulers/summary_action_scheduler.ts index 9b67c37e6216e..050eea352f0d5 100644 --- a/x-pack/plugins/alerting/server/task_runner/action_scheduler/schedulers/summary_action_scheduler.ts +++ b/x-pack/plugins/alerting/server/task_runner/action_scheduler/schedulers/summary_action_scheduler.ts @@ -8,21 +8,28 @@ import { AlertInstanceState, AlertInstanceContext } from '@kbn/alerting-state-types'; import { RuleAction, RuleTypeParams } from '@kbn/alerting-types'; import { compact } from 'lodash'; +import { CombinedSummarizedAlerts } from '../../../types'; import { RuleTypeState, RuleAlertData, parseDuration } from '../../../../common'; import { GetSummarizedAlertsParams } from '../../../alerts_client/types'; -import { getSummarizedAlerts } from '../get_summarized_alerts'; import { + buildRuleUrl, + formatActionToEnqueue, + getSummarizedAlerts, + getSummaryActionTimeBounds, isActionOnInterval, isSummaryAction, isSummaryActionThrottled, logNumberOfFilteredAlerts, -} from '../rule_action_helper'; + shouldScheduleAction, +} from '../lib'; import { ActionSchedulerOptions, - Executable, - GenerateExecutablesOpts, + ActionsToSchedule, + GetActionsToScheduleOpts, IActionScheduler, } from '../types'; +import { injectActionParams } from '../../inject_action_params'; +import { transformSummaryActionParams } from '../../transform_action_params'; export class SummaryActionScheduler< Params extends RuleTypeParams, @@ -73,13 +80,18 @@ export class SummaryActionScheduler< return 0; } - public async generateExecutables({ + public async getActionsToSchedule({ alerts, throttledSummaryActions, - }: GenerateExecutablesOpts): Promise< - Array> + }: GetActionsToScheduleOpts): Promise< + ActionsToSchedule[] > { - const executables = []; + const executables: Array<{ + action: RuleAction; + summarizedAlerts: CombinedSummarizedAlerts; + }> = []; + const results: ActionsToSchedule[] = []; + for (const action of this.actions) { if ( // if summary action is throttled, we won't send any notifications @@ -88,7 +100,7 @@ export class SummaryActionScheduler< const actionHasThrottleInterval = isActionOnInterval(action); const optionsBase = { spaceId: this.context.taskInstance.params.spaceId, - ruleId: this.context.taskInstance.params.alertId, + ruleId: this.context.rule.id, excludedAlertInstanceIds: this.context.rule.mutedInstanceIds, alertsFilter: action.alertsFilter, }; @@ -122,6 +134,95 @@ export class SummaryActionScheduler< } } - return executables; + if (executables.length === 0) return []; + + this.context.ruleRunMetricsStore.incrementNumberOfGeneratedActions(executables.length); + + for (const { action, summarizedAlerts } of executables) { + const { actionTypeId } = action; + + if ( + !shouldScheduleAction({ + action, + actionsConfigMap: this.context.taskRunnerContext.actionsConfigMap, + isActionExecutable: this.context.taskRunnerContext.actionsPlugin.isActionExecutable, + logger: this.context.logger, + ruleId: this.context.rule.id, + ruleRunMetricsStore: this.context.ruleRunMetricsStore, + }) + ) { + continue; + } + + this.context.ruleRunMetricsStore.incrementNumberOfTriggeredActions(); + this.context.ruleRunMetricsStore.incrementNumberOfTriggeredActionsByConnectorType( + actionTypeId + ); + + if (isActionOnInterval(action) && throttledSummaryActions) { + throttledSummaryActions[action.uuid!] = { date: new Date().toISOString() }; + } + + const { start, end } = getSummaryActionTimeBounds( + action, + this.context.rule.schedule, + this.context.previousStartedAt + ); + + const ruleUrl = buildRuleUrl({ + end, + getViewInAppRelativeUrl: this.context.ruleType.getViewInAppRelativeUrl, + kibanaBaseUrl: this.context.taskRunnerContext.kibanaBaseUrl, + logger: this.context.logger, + rule: this.context.rule, + spaceId: this.context.taskInstance.params.spaceId, + start, + }); + + const actionToRun = { + ...action, + params: injectActionParams({ + actionTypeId: action.actionTypeId, + ruleUrl, + ruleName: this.context.rule.name, + actionParams: transformSummaryActionParams({ + alerts: summarizedAlerts, + rule: this.context.rule, + ruleTypeId: this.context.ruleType.id, + actionId: action.id, + actionParams: action.params, + spaceId: this.context.taskInstance.params.spaceId, + actionsPlugin: this.context.taskRunnerContext.actionsPlugin, + actionTypeId: action.actionTypeId, + kibanaBaseUrl: this.context.taskRunnerContext.kibanaBaseUrl, + ruleUrl: ruleUrl?.absoluteUrl, + }), + }), + }; + + results.push({ + actionToEnqueue: formatActionToEnqueue({ + action: actionToRun, + apiKey: this.context.apiKey, + executionId: this.context.executionId, + ruleConsumer: this.context.ruleConsumer, + ruleId: this.context.rule.id, + ruleTypeId: this.context.ruleType.id, + spaceId: this.context.taskInstance.params.spaceId, + }), + actionToLog: { + id: action.id, + uuid: action.uuid, + typeId: action.actionTypeId, + alertSummary: { + new: summarizedAlerts.new.count, + ongoing: summarizedAlerts.ongoing.count, + recovered: summarizedAlerts.recovered.count, + }, + }, + }); + } + + return results; } } diff --git a/x-pack/plugins/alerting/server/task_runner/action_scheduler/schedulers/system_action_scheduler.test.ts b/x-pack/plugins/alerting/server/task_runner/action_scheduler/schedulers/system_action_scheduler.test.ts index fd4db6ce34678..28bf58a30c689 100644 --- a/x-pack/plugins/alerting/server/task_runner/action_scheduler/schedulers/system_action_scheduler.test.ts +++ b/x-pack/plugins/alerting/server/task_runner/action_scheduler/schedulers/system_action_scheduler.test.ts @@ -12,6 +12,12 @@ import { alertsClientMock } from '../../../alerts_client/alerts_client.mock'; import { alertingEventLoggerMock } from '../../../lib/alerting_event_logger/alerting_event_logger.mock'; import { RuleRunMetricsStore } from '../../../lib/rule_run_metrics_store'; import { mockAAD } from '../../fixtures'; +import { Alert } from '../../../alert'; +import { + ActionsCompletion, + AlertInstanceContext, + AlertInstanceState, +} from '@kbn/alerting-state-types'; import { getRule, getRuleType, getDefaultSchedulerContext, generateAlert } from '../test_fixtures'; import { SystemActionScheduler } from './system_action_scheduler'; import { ALERT_UUID } from '@kbn/rule-data-utils'; @@ -19,6 +25,8 @@ import { getErrorSource, TaskErrorSource, } from '@kbn/task-manager-plugin/server/task_running/errors'; +import { CombinedSummarizedAlerts } from '../../../types'; +import { schema } from '@kbn/config-schema'; const alertingEventLogger = alertingEventLoggerMock.create(); const actionsClient = actionsClientMock.create(); @@ -28,12 +36,13 @@ const logger = loggingSystemMock.create().get(); let ruleRunMetricsStore: RuleRunMetricsStore; const rule = getRule({ + id: 'rule-id-1', systemActions: [ { - id: '1', + id: 'system-action-1', actionTypeId: '.test-system-action', params: { myParams: 'test' }, - uui: 'test', + uuid: 'xxx-xxx', }, ], }); @@ -46,11 +55,43 @@ const defaultSchedulerContext = getDefaultSchedulerContext( alertsClient ); +const actionsParams = { myParams: 'test' }; +const buildActionParams = jest.fn().mockReturnValue({ ...actionsParams, foo: 'bar' }); +defaultSchedulerContext.taskRunnerContext.connectorAdapterRegistry.register({ + connectorTypeId: '.test-system-action', + ruleActionParamsSchema: schema.object({}), + buildActionParams, +}); + // @ts-ignore const getSchedulerContext = (params = {}) => { return { ...defaultSchedulerContext, rule, ...params, ruleRunMetricsStore }; }; +const getResult = (actionId: string, actionUuid: string, summary: CombinedSummarizedAlerts) => ({ + actionToEnqueue: { + actionTypeId: '.test-system-action', + apiKey: 'MTIzOmFiYw==', + consumer: 'rule-consumer', + executionId: '5f6aa57d-3e22-484e-bae8-cbed868f4d28', + id: actionId, + uuid: actionUuid, + relatedSavedObjects: [{ id: 'rule-id-1', namespace: 'test1', type: 'alert', typeId: 'test' }], + source: { source: { id: 'rule-id-1', type: 'alert' }, type: 'SAVED_OBJECT' }, + spaceId: 'test1', + }, + actionToLog: { + alertSummary: { + new: summary.new.count, + ongoing: summary.ongoing.count, + recovered: summary.recovered.count, + }, + id: actionId, + uuid: actionUuid, + typeId: '.test-system-action', + }, +}); + let clock: sinon.SinonFakeTimers; describe('System Action Scheduler', () => { @@ -88,13 +129,29 @@ describe('System Action Scheduler', () => { expect(scheduler.actions).toHaveLength(0); }); - describe('generateExecutables', () => { - const newAlert1 = generateAlert({ id: 1 }); - const newAlert2 = generateAlert({ id: 2 }); - const alerts = { ...newAlert1, ...newAlert2 }; + describe('getActionsToSchedule', () => { + let newAlert1: Record< + string, + Alert + >; + let newAlert2: Record< + string, + Alert + >; + let alerts: Record< + string, + Alert + >; - test('should generate executable for each system action', async () => { + beforeEach(() => { + newAlert1 = generateAlert({ id: 1 }); + newAlert2 = generateAlert({ id: 2 }); + alerts = { ...newAlert1, ...newAlert2 }; + }); + + test('should create actions to schedule for each system action', async () => { alertsClient.getProcessedAlerts.mockReturnValue(alerts); + const summarizedAlerts = { new: { count: 2, data: [mockAAD, mockAAD] }, ongoing: { count: 0, data: [] }, @@ -103,25 +160,27 @@ describe('System Action Scheduler', () => { alertsClient.getSummarizedAlerts.mockResolvedValue(summarizedAlerts); const scheduler = new SystemActionScheduler(getSchedulerContext()); - const executables = await scheduler.generateExecutables({ - alerts, - throttledSummaryActions: {}, - }); + const results = await scheduler.getActionsToSchedule({ alerts }); expect(alertsClient.getSummarizedAlerts).toHaveBeenCalledTimes(1); expect(alertsClient.getSummarizedAlerts).toHaveBeenCalledWith({ excludedAlertInstanceIds: [], executionUuid: defaultSchedulerContext.executionId, - ruleId: '1', + ruleId: 'rule-id-1', spaceId: 'test1', }); - expect(executables).toHaveLength(1); + expect(ruleRunMetricsStore.getNumberOfGeneratedActions()).toEqual(1); + expect(ruleRunMetricsStore.getNumberOfTriggeredActions()).toEqual(1); + expect(ruleRunMetricsStore.getStatusByConnectorType('.test-system-action')).toEqual({ + numberOfGeneratedActions: 1, + numberOfTriggeredActions: 1, + }); + + expect(results).toHaveLength(1); const finalSummary = { ...summarizedAlerts, all: { count: 2, data: [mockAAD, mockAAD] } }; - expect(executables).toEqual([ - { action: rule.systemActions?.[0], summarizedAlerts: finalSummary }, - ]); + expect(results).toEqual([getResult('system-action-1', 'xxx-xxx', finalSummary)]); }); test('should remove new alerts from summary if suppressed by maintenance window', async () => { @@ -141,22 +200,26 @@ describe('System Action Scheduler', () => { recovered: { count: 0, data: [] }, }; alertsClient.getSummarizedAlerts.mockResolvedValue(summarizedAlerts); - const scheduler = new SystemActionScheduler(getSchedulerContext()); - const executables = await scheduler.generateExecutables({ - alerts, - throttledSummaryActions: {}, - }); + const scheduler = new SystemActionScheduler(getSchedulerContext()); + const results = await scheduler.getActionsToSchedule({ alerts }); expect(alertsClient.getSummarizedAlerts).toHaveBeenCalledTimes(1); expect(alertsClient.getSummarizedAlerts).toHaveBeenCalledWith({ excludedAlertInstanceIds: [], executionUuid: defaultSchedulerContext.executionId, - ruleId: '1', + ruleId: 'rule-id-1', spaceId: 'test1', }); - expect(executables).toHaveLength(1); + expect(ruleRunMetricsStore.getNumberOfGeneratedActions()).toEqual(1); + expect(ruleRunMetricsStore.getNumberOfTriggeredActions()).toEqual(1); + expect(ruleRunMetricsStore.getStatusByConnectorType('.test-system-action')).toEqual({ + numberOfGeneratedActions: 1, + numberOfTriggeredActions: 1, + }); + + expect(results).toHaveLength(1); const finalSummary = { all: { count: 1, data: [newAADAlerts[1]] }, @@ -164,12 +227,10 @@ describe('System Action Scheduler', () => { ongoing: { count: 0, data: [] }, recovered: { count: 0, data: [] }, }; - expect(executables).toEqual([ - { action: rule.systemActions?.[0], summarizedAlerts: finalSummary }, - ]); + expect(results).toEqual([getResult('system-action-1', 'xxx-xxx', finalSummary)]); }); - test('should skip generating executable for summary action when no alerts found', async () => { + test('should skip creating actions to schedule for summary action when no alerts found', async () => { alertsClient.getProcessedAlerts.mockReturnValue(alerts); const summarizedAlerts = { new: { count: 0, data: [] }, @@ -179,21 +240,20 @@ describe('System Action Scheduler', () => { alertsClient.getSummarizedAlerts.mockResolvedValue(summarizedAlerts); const scheduler = new SystemActionScheduler(getSchedulerContext()); - - const executables = await scheduler.generateExecutables({ - alerts, - throttledSummaryActions: {}, - }); + const results = await scheduler.getActionsToSchedule({ alerts }); expect(alertsClient.getSummarizedAlerts).toHaveBeenCalledTimes(1); expect(alertsClient.getSummarizedAlerts).toHaveBeenCalledWith({ excludedAlertInstanceIds: [], executionUuid: defaultSchedulerContext.executionId, - ruleId: '1', + ruleId: 'rule-id-1', spaceId: 'test1', }); - expect(executables).toHaveLength(0); + expect(ruleRunMetricsStore.getNumberOfGeneratedActions()).toEqual(0); + expect(ruleRunMetricsStore.getNumberOfTriggeredActions()).toEqual(0); + + expect(results).toHaveLength(0); }); test('should throw framework error if getSummarizedAlerts throws error', async () => { @@ -205,14 +265,175 @@ describe('System Action Scheduler', () => { const scheduler = new SystemActionScheduler(getSchedulerContext()); try { - await scheduler.generateExecutables({ - alerts, - throttledSummaryActions: {}, - }); + await scheduler.getActionsToSchedule({ alerts }); } catch (err) { expect(err.message).toEqual(`no alerts for you`); expect(getErrorSource(err)).toBe(TaskErrorSource.FRAMEWORK); } }); + + test('should skip creating actions to schedule if overall max actions limit exceeded', async () => { + const anotherSystemAction = { + id: 'system-action-1', + actionTypeId: '.test-system-action', + params: { myParams: 'foo' }, + uuid: 'yyy-yyy', + }; + + alertsClient.getProcessedAlerts.mockReturnValue(alerts); + const summarizedAlerts = { + new: { count: 2, data: [mockAAD, mockAAD] }, + ongoing: { count: 0, data: [] }, + recovered: { count: 0, data: [] }, + }; + alertsClient.getSummarizedAlerts.mockResolvedValue(summarizedAlerts); + + const defaultContext = getSchedulerContext(); + const scheduler = new SystemActionScheduler({ + ...defaultContext, + rule: { ...rule, systemActions: [rule.systemActions?.[0]!, anotherSystemAction] }, + taskRunnerContext: { + ...defaultContext.taskRunnerContext, + actionsConfigMap: { + default: { max: 1 }, + }, + }, + }); + const results = await scheduler.getActionsToSchedule({ alerts }); + + expect(alertsClient.getSummarizedAlerts).toHaveBeenCalledTimes(2); + expect(alertsClient.getSummarizedAlerts).toHaveBeenNthCalledWith(1, { + excludedAlertInstanceIds: [], + executionUuid: defaultSchedulerContext.executionId, + ruleId: 'rule-id-1', + spaceId: 'test1', + }); + expect(alertsClient.getSummarizedAlerts).toHaveBeenNthCalledWith(2, { + excludedAlertInstanceIds: [], + executionUuid: defaultSchedulerContext.executionId, + ruleId: 'rule-id-1', + spaceId: 'test1', + }); + + expect(ruleRunMetricsStore.getNumberOfGeneratedActions()).toEqual(2); + expect(ruleRunMetricsStore.getNumberOfTriggeredActions()).toEqual(1); + expect(ruleRunMetricsStore.getStatusByConnectorType('.test-system-action')).toEqual({ + numberOfGeneratedActions: 2, + numberOfTriggeredActions: 1, + triggeredActionsStatus: ActionsCompletion.PARTIAL, + }); + + expect(logger.debug).toHaveBeenCalledWith( + `Rule "rule-id-1" skipped scheduling action "system-action-1" because the maximum number of allowed actions has been reached.` + ); + + expect(results).toHaveLength(1); + + const finalSummary = { ...summarizedAlerts, all: { count: 2, data: [mockAAD, mockAAD] } }; + expect(results).toEqual([getResult('system-action-1', 'xxx-xxx', finalSummary)]); + }); + + test('should skip creating actions to schedule if connector type max actions limit exceeded', async () => { + const anotherSystemAction = { + id: 'system-action-1', + actionTypeId: '.test-system-action', + params: { myParams: 'foo' }, + uuid: 'yyy-yyy', + }; + + alertsClient.getProcessedAlerts.mockReturnValue(alerts); + const summarizedAlerts = { + new: { count: 2, data: [mockAAD, mockAAD] }, + ongoing: { count: 0, data: [] }, + recovered: { count: 0, data: [] }, + }; + alertsClient.getSummarizedAlerts.mockResolvedValue(summarizedAlerts); + + const defaultContext = getSchedulerContext(); + const scheduler = new SystemActionScheduler({ + ...defaultContext, + rule: { ...rule, systemActions: [rule.systemActions?.[0]!, anotherSystemAction] }, + taskRunnerContext: { + ...defaultContext.taskRunnerContext, + actionsConfigMap: { + default: { max: 1000 }, + '.test-system-action': { max: 1 }, + }, + }, + }); + const results = await scheduler.getActionsToSchedule({ alerts }); + + expect(alertsClient.getSummarizedAlerts).toHaveBeenCalledTimes(2); + expect(alertsClient.getSummarizedAlerts).toHaveBeenNthCalledWith(1, { + excludedAlertInstanceIds: [], + executionUuid: defaultSchedulerContext.executionId, + ruleId: 'rule-id-1', + spaceId: 'test1', + }); + expect(alertsClient.getSummarizedAlerts).toHaveBeenNthCalledWith(2, { + excludedAlertInstanceIds: [], + executionUuid: defaultSchedulerContext.executionId, + ruleId: 'rule-id-1', + spaceId: 'test1', + }); + + expect(ruleRunMetricsStore.getNumberOfGeneratedActions()).toEqual(2); + expect(ruleRunMetricsStore.getNumberOfTriggeredActions()).toEqual(1); + expect(ruleRunMetricsStore.getStatusByConnectorType('.test-system-action')).toEqual({ + numberOfGeneratedActions: 2, + numberOfTriggeredActions: 1, + triggeredActionsStatus: ActionsCompletion.PARTIAL, + }); + + expect(logger.debug).toHaveBeenCalledWith( + `Rule "rule-id-1" skipped scheduling action "system-action-1" because the maximum number of allowed actions for connector type .test-system-action has been reached.` + ); + + expect(results).toHaveLength(1); + + const finalSummary = { ...summarizedAlerts, all: { count: 2, data: [mockAAD, mockAAD] } }; + expect(results).toEqual([getResult('system-action-1', 'xxx-xxx', finalSummary)]); + }); + + test('should skip creating actions to schedule if no connector adapter exists for connector type', async () => { + const differentSystemAction = { + id: 'different-action-1', + actionTypeId: '.test-bad-system-action', + params: { myParams: 'foo' }, + uuid: 'zzz-zzz', + }; + + alertsClient.getProcessedAlerts.mockReturnValue(alerts); + const summarizedAlerts = { + new: { count: 2, data: [mockAAD, mockAAD] }, + ongoing: { count: 0, data: [] }, + recovered: { count: 0, data: [] }, + }; + alertsClient.getSummarizedAlerts.mockResolvedValue(summarizedAlerts); + + const defaultContext = getSchedulerContext(); + const scheduler = new SystemActionScheduler({ + ...defaultContext, + rule: { ...rule, systemActions: [differentSystemAction] }, + }); + const results = await scheduler.getActionsToSchedule({ alerts }); + + expect(alertsClient.getSummarizedAlerts).toHaveBeenCalledTimes(1); + expect(alertsClient.getSummarizedAlerts).toHaveBeenCalledWith({ + excludedAlertInstanceIds: [], + executionUuid: defaultSchedulerContext.executionId, + ruleId: 'rule-id-1', + spaceId: 'test1', + }); + + expect(ruleRunMetricsStore.getNumberOfGeneratedActions()).toEqual(1); + expect(ruleRunMetricsStore.getNumberOfTriggeredActions()).toEqual(0); + + expect(logger.warn).toHaveBeenCalledWith( + `Rule "rule-id-1" skipped scheduling system action "different-action-1" because no connector adapter is configured` + ); + + expect(results).toHaveLength(0); + }); }); }); diff --git a/x-pack/plugins/alerting/server/task_runner/action_scheduler/schedulers/system_action_scheduler.ts b/x-pack/plugins/alerting/server/task_runner/action_scheduler/schedulers/system_action_scheduler.ts index b923baf8fbf38..0c5cceb0f0a52 100644 --- a/x-pack/plugins/alerting/server/task_runner/action_scheduler/schedulers/system_action_scheduler.ts +++ b/x-pack/plugins/alerting/server/task_runner/action_scheduler/schedulers/system_action_scheduler.ts @@ -7,13 +7,19 @@ import { AlertInstanceState, AlertInstanceContext } from '@kbn/alerting-state-types'; import { RuleSystemAction, RuleTypeParams } from '@kbn/alerting-types'; +import { CombinedSummarizedAlerts } from '../../../types'; import { RuleTypeState, RuleAlertData } from '../../../../common'; import { GetSummarizedAlertsParams } from '../../../alerts_client/types'; -import { getSummarizedAlerts } from '../get_summarized_alerts'; +import { + buildRuleUrl, + formatActionToEnqueue, + getSummarizedAlerts, + shouldScheduleAction, +} from '../lib'; import { ActionSchedulerOptions, - Executable, - GenerateExecutablesOpts, + ActionsToSchedule, + GetActionsToScheduleOpts, IActionScheduler, } from '../types'; @@ -53,14 +59,19 @@ export class SystemActionScheduler< return 1; } - public async generateExecutables( - _: GenerateExecutablesOpts - ): Promise>> { - const executables = []; + public async getActionsToSchedule( + _: GetActionsToScheduleOpts + ): Promise { + const executables: Array<{ + action: RuleSystemAction; + summarizedAlerts: CombinedSummarizedAlerts; + }> = []; + const results: ActionsToSchedule[] = []; + for (const action of this.actions) { const options: GetSummarizedAlertsParams = { spaceId: this.context.taskInstance.params.spaceId, - ruleId: this.context.taskInstance.params.alertId, + ruleId: this.context.rule.id, excludedAlertInstanceIds: this.context.rule.mutedInstanceIds, executionUuid: this.context.executionId, }; @@ -75,6 +86,95 @@ export class SystemActionScheduler< } } - return executables; + if (executables.length === 0) return []; + + this.context.ruleRunMetricsStore.incrementNumberOfGeneratedActions(executables.length); + + const ruleUrl = buildRuleUrl({ + getViewInAppRelativeUrl: this.context.ruleType.getViewInAppRelativeUrl, + kibanaBaseUrl: this.context.taskRunnerContext.kibanaBaseUrl, + logger: this.context.logger, + rule: this.context.rule, + spaceId: this.context.taskInstance.params.spaceId, + }); + + for (const { action, summarizedAlerts } of executables) { + const { actionTypeId } = action; + + if ( + !shouldScheduleAction({ + action, + actionsConfigMap: this.context.taskRunnerContext.actionsConfigMap, + isActionExecutable: this.context.taskRunnerContext.actionsPlugin.isActionExecutable, + logger: this.context.logger, + ruleId: this.context.rule.id, + ruleRunMetricsStore: this.context.ruleRunMetricsStore, + }) + ) { + continue; + } + + const hasConnectorAdapter = this.context.taskRunnerContext.connectorAdapterRegistry.has( + action.actionTypeId + ); + + // System actions without an adapter cannot be executed + if (!hasConnectorAdapter) { + this.context.logger.warn( + `Rule "${this.context.rule.id}" skipped scheduling system action "${action.id}" because no connector adapter is configured` + ); + + continue; + } + + this.context.ruleRunMetricsStore.incrementNumberOfTriggeredActions(); + this.context.ruleRunMetricsStore.incrementNumberOfTriggeredActionsByConnectorType( + actionTypeId + ); + + const connectorAdapter = this.context.taskRunnerContext.connectorAdapterRegistry.get( + action.actionTypeId + ); + + const connectorAdapterActionParams = connectorAdapter.buildActionParams({ + alerts: summarizedAlerts, + rule: { + id: this.context.rule.id, + tags: this.context.rule.tags, + name: this.context.rule.name, + consumer: this.context.rule.consumer, + producer: this.context.ruleType.producer, + }, + ruleUrl: ruleUrl?.absoluteUrl, + spaceId: this.context.taskInstance.params.spaceId, + params: action.params, + }); + + const actionToRun = Object.assign(action, { params: connectorAdapterActionParams }); + + results.push({ + actionToEnqueue: formatActionToEnqueue({ + action: actionToRun, + apiKey: this.context.apiKey, + executionId: this.context.executionId, + ruleConsumer: this.context.ruleConsumer, + ruleId: this.context.rule.id, + ruleTypeId: this.context.ruleType.id, + spaceId: this.context.taskInstance.params.spaceId, + }), + actionToLog: { + id: action.id, + uuid: action.uuid, + typeId: action.actionTypeId, + alertSummary: { + new: summarizedAlerts.new.count, + ongoing: summarizedAlerts.ongoing.count, + recovered: summarizedAlerts.recovered.count, + }, + }, + }); + } + + return results; } } diff --git a/x-pack/plugins/alerting/server/task_runner/action_scheduler/types.ts b/x-pack/plugins/alerting/server/task_runner/action_scheduler/types.ts index efcb51fcb2698..b90ffb88d541b 100644 --- a/x-pack/plugins/alerting/server/task_runner/action_scheduler/types.ts +++ b/x-pack/plugins/alerting/server/task_runner/action_scheduler/types.ts @@ -8,6 +8,7 @@ import type { Logger } from '@kbn/core/server'; import { PublicMethodsOf } from '@kbn/utility-types'; import { ActionsClient } from '@kbn/actions-plugin/server/actions_client'; +import { ExecuteOptions as EnqueueExecutionOptions } from '@kbn/actions-plugin/server/create_execute_function'; import { IAlertsClient } from '../../alerts_client/types'; import { Alert } from '../../alert'; import { @@ -24,7 +25,10 @@ import { import { NormalizedRuleType } from '../../rule_type_registry'; import { CombinedSummarizedAlerts, RawRule } from '../../types'; import { RuleRunMetricsStore } from '../../lib/rule_run_metrics_store'; -import { AlertingEventLogger } from '../../lib/alerting_event_logger/alerting_event_logger'; +import { + ActionOpts, + AlertingEventLogger, +} from '../../lib/alerting_event_logger/alerting_event_logger'; import { RuleTaskInstance, TaskRunnerContext } from '../types'; export interface ActionSchedulerOptions< @@ -80,14 +84,19 @@ export type Executable< } ); -export interface GenerateExecutablesOpts< +export interface GetActionsToScheduleOpts< State extends AlertInstanceState, Context extends AlertInstanceContext, ActionGroupIds extends string, RecoveryActionGroupId extends string > { alerts: Record>; - throttledSummaryActions: ThrottledActions; + throttledSummaryActions?: ThrottledActions; +} + +export interface ActionsToSchedule { + actionToEnqueue: EnqueueExecutionOptions; + actionToLog: ActionOpts; } export interface IActionScheduler< @@ -97,9 +106,9 @@ export interface IActionScheduler< RecoveryActionGroupId extends string > { get priority(): number; - generateExecutables( - opts: GenerateExecutablesOpts - ): Promise>>; + getActionsToSchedule( + opts: GetActionsToScheduleOpts + ): Promise; } export interface RuleUrl { diff --git a/x-pack/plugins/alerting/server/task_runner/fixtures.ts b/x-pack/plugins/alerting/server/task_runner/fixtures.ts index 5174aa9b965ec..d820f2690caeb 100644 --- a/x-pack/plugins/alerting/server/task_runner/fixtures.ts +++ b/x-pack/plugins/alerting/server/task_runner/fixtures.ts @@ -21,7 +21,7 @@ import { import { getDefaultMonitoring } from '../lib/monitoring'; import { UntypedNormalizedRuleType } from '../rule_type_registry'; import { EVENT_LOG_ACTIONS } from '../plugin'; -import { RawRule } from '../types'; +import { AlertHit, RawRule } from '../types'; import { RULE_SAVED_OBJECT_TYPE } from '../saved_objects'; interface GeneratorParams { @@ -349,9 +349,10 @@ export const generateAlertOpts = ({ }; }; -export const generateActionOpts = ({ id, alertGroup, alertId }: GeneratorParams = {}) => ({ +export const generateActionOpts = ({ id, alertGroup, alertId, uuid }: GeneratorParams = {}) => ({ id: id ?? '1', typeId: 'action', + uuid: uuid ?? '111-111', alertId: alertId ?? '1', alertGroup: alertGroup ?? 'default', }); @@ -403,11 +404,13 @@ export const generateRunnerResult = ({ export const generateEnqueueFunctionInput = ({ id = '1', + uuid = '111-111', isBulk = false, isResolved, foo, actionTypeId, }: { + uuid?: string; id: string; isBulk?: boolean; isResolved?: boolean; @@ -419,6 +422,7 @@ export const generateEnqueueFunctionInput = ({ apiKey: 'MTIzOmFiYw==', executionId: '5f6aa57d-3e22-484e-bae8-cbed868f4d28', id, + uuid, params: { ...(isResolved !== undefined ? { isResolved } : {}), ...(foo !== undefined ? { foo } : {}), @@ -504,4 +508,4 @@ export const mockAAD = { }, }, }, -}; +} as unknown as AlertHit; diff --git a/x-pack/plugins/alerting/server/task_runner/task_runner.test.ts b/x-pack/plugins/alerting/server/task_runner/task_runner.test.ts index eb531f0e00b88..b6e59402ba4c6 100644 --- a/x-pack/plugins/alerting/server/task_runner/task_runner.test.ts +++ b/x-pack/plugins/alerting/server/task_runner/task_runner.test.ts @@ -1420,7 +1420,7 @@ describe('Task Runner', () => { expect(alertingEventLogger.logAction).toHaveBeenNthCalledWith(1, generateActionOpts({})); expect(alertingEventLogger.logAction).toHaveBeenNthCalledWith( 2, - generateActionOpts({ id: '2', alertId: '2', alertGroup: 'recovered' }) + generateActionOpts({ id: '2', alertId: '2', alertGroup: 'recovered', uuid: '222-222' }) ); expect(enqueueFunction).toHaveBeenCalledTimes(isBulk ? 1 : 2); @@ -1428,7 +1428,12 @@ describe('Task Runner', () => { isBulk ? [ generateEnqueueFunctionInput({ isBulk: false, id: '1', foo: true }), - generateEnqueueFunctionInput({ isBulk: false, id: '2', isResolved: true }), + generateEnqueueFunctionInput({ + isBulk: false, + id: '2', + isResolved: true, + uuid: '222-222', + }), ] : generateEnqueueFunctionInput({ isBulk: false, id: '1', foo: true }) ); @@ -1645,7 +1650,12 @@ describe('Task Runner', () => { isBulk ? [ generateEnqueueFunctionInput({ isBulk: false, id: '1', foo: true }), - generateEnqueueFunctionInput({ isBulk: false, id: '2', isResolved: true }), + generateEnqueueFunctionInput({ + isBulk: false, + id: '2', + isResolved: true, + uuid: '222-222', + }), ] : generateEnqueueFunctionInput({ isBulk: false, id: '1', foo: true }) ); @@ -2891,26 +2901,31 @@ describe('Task Runner', () => { { group: 'default', id: '1', + uuid: '111-111', actionTypeId: 'action', }, { group: 'default', id: '2', + uuid: '222-222', actionTypeId: 'action', }, { group: 'default', id: '3', + uuid: '333-333', actionTypeId: 'action', }, { group: 'default', id: '4', + uuid: '444-444', actionTypeId: 'action', }, { group: 'default', id: '5', + uuid: '555-555', actionTypeId: 'action', }, ]; @@ -2975,7 +2990,7 @@ describe('Task Runner', () => { }) ); - expect(logger.debug).toHaveBeenCalledTimes(7); + expect(logger.debug).toHaveBeenCalledTimes(8); expect(logger.debug).nthCalledWith( 3, @@ -3012,11 +3027,11 @@ describe('Task Runner', () => { expect(alertingEventLogger.logAction).toHaveBeenNthCalledWith(1, generateActionOpts({})); expect(alertingEventLogger.logAction).toHaveBeenNthCalledWith( 2, - generateActionOpts({ id: '2' }) + generateActionOpts({ id: '2', uuid: '222-222' }) ); expect(alertingEventLogger.logAction).toHaveBeenNthCalledWith( 3, - generateActionOpts({ id: '3' }) + generateActionOpts({ id: '3', uuid: '333-333' }) ); }); @@ -3061,26 +3076,31 @@ describe('Task Runner', () => { { group: 'default', id: '1', + uuid: '111-111', actionTypeId: '.server-log', }, { group: 'default', id: '2', + uuid: '222-222', actionTypeId: '.server-log', }, { group: 'default', id: '3', + uuid: '333-333', actionTypeId: '.server-log', }, { group: 'default', id: '4', + uuid: '444-444', actionTypeId: 'any-action', }, { group: 'default', id: '5', + uuid: '555-555', actionTypeId: 'any-action', }, ] as RuleAction[], @@ -3176,7 +3196,7 @@ describe('Task Runner', () => { status: 'warning', errorReason: `maxExecutableActions`, logAlert: 4, - logAction: 3, + logAction: 5, }); }); diff --git a/x-pack/plugins/cases/common/utils/owner.test.ts b/x-pack/plugins/cases/common/utils/owner.test.ts index d3de319754725..a6f7c99f540bb 100644 --- a/x-pack/plugins/cases/common/utils/owner.test.ts +++ b/x-pack/plugins/cases/common/utils/owner.test.ts @@ -42,7 +42,7 @@ describe('owner utils', () => { it.each(owners)('returns owner %s correctly for consumer', (owner) => { for (const consumer of owner.validRuleConsumers ?? []) { - const result = getOwnerFromRuleConsumerProducer(consumer); + const result = getOwnerFromRuleConsumerProducer({ consumer }); expect(result).toBe(owner.id); } @@ -50,23 +50,33 @@ describe('owner utils', () => { it.each(owners)('returns owner %s correctly for producer', (owner) => { for (const producer of owner.validRuleConsumers ?? []) { - const result = getOwnerFromRuleConsumerProducer(undefined, producer); + const result = getOwnerFromRuleConsumerProducer({ producer }); expect(result).toBe(owner.id); } }); it('returns cases as a default owner', () => { - const owner = getOwnerFromRuleConsumerProducer(); + const owner = getOwnerFromRuleConsumerProducer({}); expect(owner).toBe(OWNER_INFO.cases.id); }); - it('returns owner as per consumer when both values are passed ', () => { - const owner = getOwnerFromRuleConsumerProducer( - AlertConsumers.SIEM, - AlertConsumers.OBSERVABILITY - ); + it('returns owner as per consumer when both values are passed', () => { + const owner = getOwnerFromRuleConsumerProducer({ + consumer: AlertConsumers.SIEM, + producer: AlertConsumers.OBSERVABILITY, + }); + + expect(owner).toBe(OWNER_INFO.securitySolution.id); + }); + + it('returns securitySolution owner if project isServerlessSecurity', () => { + const owner = getOwnerFromRuleConsumerProducer({ + consumer: AlertConsumers.OBSERVABILITY, + producer: AlertConsumers.OBSERVABILITY, + isServerlessSecurity: true, + }); expect(owner).toBe(OWNER_INFO.securitySolution.id); }); diff --git a/x-pack/plugins/cases/common/utils/owner.ts b/x-pack/plugins/cases/common/utils/owner.ts index d3650f0995b86..7bde7220233db 100644 --- a/x-pack/plugins/cases/common/utils/owner.ts +++ b/x-pack/plugins/cases/common/utils/owner.ts @@ -14,7 +14,21 @@ export const isValidOwner = (owner: string): owner is keyof typeof OWNER_INFO => export const getCaseOwnerByAppId = (currentAppId?: string) => Object.values(OWNER_INFO).find((info) => info.appId === currentAppId)?.id; -export const getOwnerFromRuleConsumerProducer = (consumer?: string, producer?: string): Owner => { +export const getOwnerFromRuleConsumerProducer = ({ + consumer, + producer, + isServerlessSecurity, +}: { + consumer?: string; + producer?: string; + isServerlessSecurity?: boolean; +}): Owner => { + // This is a workaround for a very specific bug with the cases action in serverless security + // More info here: https://github.com/elastic/kibana/issues/186270 + if (isServerlessSecurity) { + return OWNER_INFO.securitySolution.id; + } + for (const value of Object.values(OWNER_INFO)) { const foundConsumer = value.validRuleConsumers?.find( (validConsumer) => validConsumer === consumer || validConsumer === producer diff --git a/x-pack/plugins/cases/kibana.jsonc b/x-pack/plugins/cases/kibana.jsonc index 84c04da1fe0f6..300b1ee4c2c12 100644 --- a/x-pack/plugins/cases/kibana.jsonc +++ b/x-pack/plugins/cases/kibana.jsonc @@ -30,6 +30,7 @@ "uiActions", ], "optionalPlugins": [ + "cloud", "home", "taskManager", "usageCollection", diff --git a/x-pack/plugins/cases/public/components/system_actions/cases/cases_params.tsx b/x-pack/plugins/cases/public/components/system_actions/cases/cases_params.tsx index 6b6f85e53045f..6c93b2435af8e 100644 --- a/x-pack/plugins/cases/public/components/system_actions/cases/cases_params.tsx +++ b/x-pack/plugins/cases/public/components/system_actions/cases/cases_params.tsx @@ -43,7 +43,7 @@ export const CasesParamsFieldsComponent: React.FunctionComponent< notifications: { toasts }, data: { dataViews: dataViewsService }, } = useKibana().services; - const owner = getOwnerFromRuleConsumerProducer(featureId, producerId); + const owner = getOwnerFromRuleConsumerProducer({ consumer: featureId, producer: producerId }); const { dataView, isLoading: loadingAlertDataViews } = useAlertsDataView({ http, diff --git a/x-pack/plugins/cases/server/connectors/cases/index.test.ts b/x-pack/plugins/cases/server/connectors/cases/index.test.ts index cca7092fde368..5c7b29ef4e704 100644 --- a/x-pack/plugins/cases/server/connectors/cases/index.test.ts +++ b/x-pack/plugins/cases/server/connectors/cases/index.test.ts @@ -80,26 +80,26 @@ describe('getCasesConnectorType', () => { }); it('sets the correct connectorTypeId', () => { - const adapter = getCasesConnectorAdapter(); + const adapter = getCasesConnectorAdapter({}); expect(adapter.connectorTypeId).toEqual('.cases'); }); describe('ruleActionParamsSchema', () => { it('validates getParams() correctly', () => { - const adapter = getCasesConnectorAdapter(); + const adapter = getCasesConnectorAdapter({}); expect(adapter.ruleActionParamsSchema.validate(getParams())).toEqual(getParams()); }); it('throws if missing getParams()', () => { - const adapter = getCasesConnectorAdapter(); + const adapter = getCasesConnectorAdapter({}); expect(() => adapter.ruleActionParamsSchema.validate({})).toThrow(); }); it('does not accept more than one groupingBy key', () => { - const adapter = getCasesConnectorAdapter(); + const adapter = getCasesConnectorAdapter({}); expect(() => adapter.ruleActionParamsSchema.validate( @@ -109,7 +109,7 @@ describe('getCasesConnectorType', () => { }); it('should fail with not valid time window', () => { - const adapter = getCasesConnectorAdapter(); + const adapter = getCasesConnectorAdapter({}); expect(() => adapter.ruleActionParamsSchema.validate(getParams({ timeWindow: '10d+3d' })) @@ -119,7 +119,7 @@ describe('getCasesConnectorType', () => { describe('buildActionParams', () => { it('builds the action getParams() correctly', () => { - const adapter = getCasesConnectorAdapter(); + const adapter = getCasesConnectorAdapter({}); expect( adapter.buildActionParams({ @@ -164,7 +164,7 @@ describe('getCasesConnectorType', () => { }); it('builds the action getParams() and templateId correctly', () => { - const adapter = getCasesConnectorAdapter(); + const adapter = getCasesConnectorAdapter({}); expect( adapter.buildActionParams({ @@ -209,7 +209,7 @@ describe('getCasesConnectorType', () => { }); it('builds the action getParams() correctly without ruleUrl', () => { - const adapter = getCasesConnectorAdapter(); + const adapter = getCasesConnectorAdapter({}); expect( adapter.buildActionParams({ // @ts-expect-error: not all fields are needed @@ -252,7 +252,7 @@ describe('getCasesConnectorType', () => { }); it('maps observability consumers to the correct owner', () => { - const adapter = getCasesConnectorAdapter(); + const adapter = getCasesConnectorAdapter({}); for (const consumer of [ AlertConsumers.OBSERVABILITY, @@ -276,7 +276,7 @@ describe('getCasesConnectorType', () => { }); it('maps security solution consumers to the correct owner', () => { - const adapter = getCasesConnectorAdapter(); + const adapter = getCasesConnectorAdapter({}); for (const consumer of [AlertConsumers.SIEM]) { const connectorParams = adapter.buildActionParams({ @@ -292,7 +292,7 @@ describe('getCasesConnectorType', () => { }); it('maps stack consumers to the correct owner', () => { - const adapter = getCasesConnectorAdapter(); + const adapter = getCasesConnectorAdapter({}); for (const consumer of [AlertConsumers.ML, AlertConsumers.STACK_ALERTS]) { const connectorParams = adapter.buildActionParams({ @@ -308,7 +308,7 @@ describe('getCasesConnectorType', () => { }); it('fallback to the cases owner if the consumer is not in the mapping', () => { - const adapter = getCasesConnectorAdapter(); + const adapter = getCasesConnectorAdapter({}); const connectorParams = adapter.buildActionParams({ // @ts-expect-error: not all fields are needed @@ -320,11 +320,27 @@ describe('getCasesConnectorType', () => { expect(connectorParams.subActionParams.owner).toBe('cases'); }); + + it('correctly fallsback to security owner if the project is serverless security', () => { + const adapter = getCasesConnectorAdapter({ isServerlessSecurity: true }); + + for (const consumer of [AlertConsumers.ML, AlertConsumers.STACK_ALERTS]) { + const connectorParams = adapter.buildActionParams({ + // @ts-expect-error: not all fields are needed + alerts, + rule: { ...rule, consumer }, + params: getParams(), + spaceId: 'default', + }); + + expect(connectorParams.subActionParams.owner).toBe('securitySolution'); + } + }); }); describe('getKibanaPrivileges', () => { it('constructs the correct privileges from the consumer', () => { - const adapter = getCasesConnectorAdapter(); + const adapter = getCasesConnectorAdapter({}); expect( adapter.getKibanaPrivileges?.({ @@ -344,7 +360,7 @@ describe('getCasesConnectorType', () => { }); it('constructs the correct privileges from the producer if the consumer is not found', () => { - const adapter = getCasesConnectorAdapter(); + const adapter = getCasesConnectorAdapter({}); expect( adapter.getKibanaPrivileges?.({ @@ -362,6 +378,26 @@ describe('getCasesConnectorType', () => { 'cases:observability/findConfigurations', ]); }); + + it('correctly overrides the consumer and producer if the project is serverless security', () => { + const adapter = getCasesConnectorAdapter({ isServerlessSecurity: true }); + + expect( + adapter.getKibanaPrivileges?.({ + consumer: 'alerting', + producer: AlertConsumers.LOGS, + }) + ).toEqual([ + 'cases:securitySolution/createCase', + 'cases:securitySolution/updateCase', + 'cases:securitySolution/deleteCase', + 'cases:securitySolution/pushCase', + 'cases:securitySolution/createComment', + 'cases:securitySolution/updateComment', + 'cases:securitySolution/deleteComment', + 'cases:securitySolution/findConfigurations', + ]); + }); }); }); }); diff --git a/x-pack/plugins/cases/server/connectors/cases/index.ts b/x-pack/plugins/cases/server/connectors/cases/index.ts index 8be0b645cbfb3..07b4ab5e29551 100644 --- a/x-pack/plugins/cases/server/connectors/cases/index.ts +++ b/x-pack/plugins/cases/server/connectors/cases/index.ts @@ -16,7 +16,11 @@ import type { SavedObjectsClientContract } from '@kbn/core/server'; import type { ConnectorAdapter } from '@kbn/alerting-plugin/server'; import { CasesConnector } from './cases_connector'; import { DEFAULT_MAX_OPEN_CASES } from './constants'; -import { CASES_CONNECTOR_ID, CASES_CONNECTOR_TITLE } from '../../../common/constants'; +import { + CASES_CONNECTOR_ID, + CASES_CONNECTOR_TITLE, + SECURITY_SOLUTION_OWNER, +} from '../../../common/constants'; import { getOwnerFromRuleConsumerProducer } from '../../../common/utils/owner'; import type { @@ -40,12 +44,14 @@ interface GetCasesConnectorTypeArgs { savedObjectTypes: string[] ) => Promise; getSpaceId: (request?: KibanaRequest) => string; + isServerlessSecurity?: boolean; } export const getCasesConnectorType = ({ getCasesClient, getSpaceId, getUnsecuredSavedObjectsClient, + isServerlessSecurity, }: GetCasesConnectorTypeArgs): SubActionConnectorType< CasesConnectorConfig, CasesConnectorSecrets @@ -69,27 +75,34 @@ export const getCasesConnectorType = ({ minimumLicenseRequired: 'platinum' as const, isSystemActionType: true, getKibanaPrivileges: ({ params } = { params: { subAction: 'run', subActionParams: {} } }) => { - const owner = params?.subActionParams?.owner as string; - - if (!owner) { + if (!params?.subActionParams?.owner) { throw new Error('Cannot authorize cases. Owner is not defined in the subActionParams.'); } + const owner = isServerlessSecurity + ? SECURITY_SOLUTION_OWNER + : (params?.subActionParams?.owner as string); + return constructRequiredKibanaPrivileges(owner); }, }); -export const getCasesConnectorAdapter = (): ConnectorAdapter< - CasesConnectorRuleActionParams, - CasesConnectorParams -> => { +export const getCasesConnectorAdapter = ({ + isServerlessSecurity, +}: { + isServerlessSecurity?: boolean; +}): ConnectorAdapter => { return { connectorTypeId: CASES_CONNECTOR_ID, ruleActionParamsSchema: CasesConnectorRuleActionParamsSchema, buildActionParams: ({ alerts, rule, params, spaceId, ruleUrl }) => { const caseAlerts = [...alerts.new.data, ...alerts.ongoing.data]; - const owner = getOwnerFromRuleConsumerProducer(rule.consumer, rule.producer); + const owner = getOwnerFromRuleConsumerProducer({ + consumer: rule.consumer, + producer: rule.producer, + isServerlessSecurity, + }); const subActionParams = { alerts: caseAlerts, @@ -105,7 +118,7 @@ export const getCasesConnectorAdapter = (): ConnectorAdapter< return { subAction: 'run', subActionParams }; }, getKibanaPrivileges: ({ consumer, producer }) => { - const owner = getOwnerFromRuleConsumerProducer(consumer, producer); + const owner = getOwnerFromRuleConsumerProducer({ consumer, producer, isServerlessSecurity }); return constructRequiredKibanaPrivileges(owner); }, }; diff --git a/x-pack/plugins/cases/server/connectors/index.ts b/x-pack/plugins/cases/server/connectors/index.ts index 2d680163dde28..0b0f201b46d42 100644 --- a/x-pack/plugins/cases/server/connectors/index.ts +++ b/x-pack/plugins/cases/server/connectors/index.ts @@ -22,12 +22,14 @@ export function registerConnectorTypes({ core, getCasesClient, getSpaceId, + isServerlessSecurity, }: { actions: ActionsPluginSetupContract; alerting: AlertingPluginSetup; core: CoreSetup; getCasesClient: (request: KibanaRequest) => Promise; getSpaceId: (request?: KibanaRequest) => string; + isServerlessSecurity?: boolean; }) { const getUnsecuredSavedObjectsClient = async ( request: KibanaRequest, @@ -53,8 +55,13 @@ export function registerConnectorTypes({ }; actions.registerSubActionConnectorType( - getCasesConnectorType({ getCasesClient, getSpaceId, getUnsecuredSavedObjectsClient }) + getCasesConnectorType({ + getCasesClient, + getSpaceId, + getUnsecuredSavedObjectsClient, + isServerlessSecurity, + }) ); - alerting.registerConnectorAdapter(getCasesConnectorAdapter()); + alerting.registerConnectorAdapter(getCasesConnectorAdapter({ isServerlessSecurity })); } diff --git a/x-pack/plugins/cases/server/plugin.ts b/x-pack/plugins/cases/server/plugin.ts index 262289bc6a24c..fa172b48520a7 100644 --- a/x-pack/plugins/cases/server/plugin.ts +++ b/x-pack/plugins/cases/server/plugin.ts @@ -145,12 +145,16 @@ export class CasePlugin return plugins.spaces?.spacesService.getSpaceId(request) ?? DEFAULT_SPACE_ID; }; + const isServerlessSecurity = + plugins.cloud?.isServerlessEnabled && plugins.cloud?.serverless.projectType === 'security'; + registerConnectorTypes({ actions: plugins.actions, alerting: plugins.alerting, core, getCasesClient, getSpaceId, + isServerlessSecurity, }); return { diff --git a/x-pack/plugins/cases/server/types.ts b/x-pack/plugins/cases/server/types.ts index 0153083337cfa..a51817c9d7e58 100644 --- a/x-pack/plugins/cases/server/types.ts +++ b/x-pack/plugins/cases/server/types.ts @@ -30,6 +30,7 @@ import type { LicensingPluginSetup, LicensingPluginStart } from '@kbn/licensing- import type { NotificationsPluginStart } from '@kbn/notifications-plugin/server'; import type { RuleRegistryPluginStartContract } from '@kbn/rule-registry-plugin/server'; import type { PluginSetupContract as AlertingPluginSetup } from '@kbn/alerting-plugin/server'; +import type { CloudSetup } from '@kbn/cloud-plugin/server'; import type { CasesClient } from './client'; import type { AttachmentFramework } from './attachment_framework/types'; import type { ExternalReferenceAttachmentTypeRegistry } from './attachment_framework/external_reference_registry'; @@ -46,6 +47,7 @@ export interface CasesServerSetupDependencies { taskManager?: TaskManagerSetupContract; usageCollection?: UsageCollectionSetup; spaces?: SpacesPluginSetup; + cloud?: CloudSetup; } export interface CasesServerStartDependencies { diff --git a/x-pack/plugins/cases/tsconfig.json b/x-pack/plugins/cases/tsconfig.json index fbe05134b5bfd..1d126d78f9543 100644 --- a/x-pack/plugins/cases/tsconfig.json +++ b/x-pack/plugins/cases/tsconfig.json @@ -76,6 +76,7 @@ "@kbn/core-http-router-server-internal", "@kbn/presentation-publishing", "@kbn/alerts-ui-shared", + "@kbn/cloud-plugin", ], "exclude": [ "target/**/*", diff --git a/x-pack/plugins/cloud_security_posture/common/constants.ts b/x-pack/plugins/cloud_security_posture/common/constants.ts index d415d4cfcfc69..474f29b859305 100644 --- a/x-pack/plugins/cloud_security_posture/common/constants.ts +++ b/x-pack/plugins/cloud_security_posture/common/constants.ts @@ -171,4 +171,4 @@ export const SINGLE_ACCOUNT = 'single-account'; export const CLOUD_SECURITY_PLUGIN_VERSION = '1.9.0'; // Cloud Credentials Template url was implemented in 1.10.0-preview01. See PR - https://github.com/elastic/integrations/pull/9828 -export const CLOUD_CREDENTIALS_PACKAGE_VERSION = '1.10.0-preview01'; +export const CLOUD_CREDENTIALS_PACKAGE_VERSION = '1.11.0-preview10'; diff --git a/x-pack/plugins/cloud_security_posture/public/common/utils/get_vulnerability_colors.ts b/x-pack/plugins/cloud_security_posture/public/common/utils/get_vulnerability_colors.ts deleted file mode 100644 index cba51677dd58a..0000000000000 --- a/x-pack/plugins/cloud_security_posture/public/common/utils/get_vulnerability_colors.ts +++ /dev/null @@ -1,20 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { euiThemeVars } from '@kbn/ui-theme'; - -export const getCvsScoreColor = (score: number): string | undefined => { - if (score <= 4) { - return euiThemeVars.euiColorVis0; // low severity - } else if (score >= 4 && score <= 7) { - return euiThemeVars.euiColorVis7; // medium severity - } else if (score >= 7 && score <= 9) { - return euiThemeVars.euiColorVis9; // high severity - } else if (score >= 9) { - return euiThemeVars.euiColorDanger; // critical severity - } -}; diff --git a/x-pack/plugins/cloud_security_posture/public/common/utils/get_vulnerabiltity_colors.test.ts b/x-pack/plugins/cloud_security_posture/public/common/utils/get_vulnerabiltity_colors.test.ts deleted file mode 100644 index 5000e14a5afc6..0000000000000 --- a/x-pack/plugins/cloud_security_posture/public/common/utils/get_vulnerabiltity_colors.test.ts +++ /dev/null @@ -1,31 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { euiThemeVars } from '@kbn/ui-theme'; -import { getCvsScoreColor } from './get_vulnerability_colors'; - -describe('getCvsScoreColor', () => { - it('returns correct color for low severity score', () => { - expect(getCvsScoreColor(1.5)).toBe(euiThemeVars.euiColorVis0); - }); - - it('returns correct color for medium severity score', () => { - expect(getCvsScoreColor(5.5)).toBe(euiThemeVars.euiColorVis7); - }); - - it('returns correct color for high severity score', () => { - expect(getCvsScoreColor(7.9)).toBe(euiThemeVars.euiColorVis9); - }); - - it('returns correct color for critical severity score', () => { - expect(getCvsScoreColor(10.0)).toBe(euiThemeVars.euiColorDanger); - }); - - it('returns correct color for low severity score for undefined value', () => { - expect(getCvsScoreColor(-0.2)).toBe(euiThemeVars.euiColorVis0); - }); -}); diff --git a/x-pack/plugins/cloud_security_posture/public/components/fleet_extensions/policy_template_form.tsx b/x-pack/plugins/cloud_security_posture/public/components/fleet_extensions/policy_template_form.tsx index 721c4ca147aee..73d8ed22011dc 100644 --- a/x-pack/plugins/cloud_security_posture/public/components/fleet_extensions/policy_template_form.tsx +++ b/x-pack/plugins/cloud_security_posture/public/components/fleet_extensions/policy_template_form.tsx @@ -674,6 +674,7 @@ export const CspPolicyTemplateForm = memo ({ id: v, label: getPolicyTemplateLabel(v) }))} + options={Array.from(policyTemplates, (v) => ({ + id: v, + label: getPolicyTemplateLabel(v), + testId: `policy-template-radio-button-${v}`, + }))} idSelected={selectedTemplate} onChange={(id: CloudSecurityPolicyTemplate) => setPolicyTemplate(id)} disabled={disabled} diff --git a/x-pack/plugins/cloud_security_posture/public/components/fleet_extensions/setup_technology_selector/use_setup_technology.ts b/x-pack/plugins/cloud_security_posture/public/components/fleet_extensions/setup_technology_selector/use_setup_technology.ts index 8b6a190827f2e..e18119c3a39de 100644 --- a/x-pack/plugins/cloud_security_posture/public/components/fleet_extensions/setup_technology_selector/use_setup_technology.ts +++ b/x-pack/plugins/cloud_security_posture/public/components/fleet_extensions/setup_technology_selector/use_setup_technology.ts @@ -27,7 +27,8 @@ export const useSetupTechnology = ({ const isAgentlessSupportedForCloudProvider = isCspmAws || isCspmGcp || isCspmAzure; const isAgentlessAvailable = isAgentlessSupportedForCloudProvider && isAgentlessEnabled; const defaultSetupTechnology = - isEditPage && isAgentlessEnabled ? SetupTechnology.AGENTLESS : SetupTechnology.AGENT_BASED; + isEditPage && isAgentlessAvailable ? SetupTechnology.AGENTLESS : SetupTechnology.AGENT_BASED; + const [setupTechnology, setSetupTechnology] = useState(defaultSetupTechnology); const updateSetupTechnology = (value: SetupTechnology) => { diff --git a/x-pack/plugins/cloud_security_posture/public/components/test_subjects.ts b/x-pack/plugins/cloud_security_posture/public/components/test_subjects.ts index 6e17a59b3ba08..d29971d3352e3 100644 --- a/x-pack/plugins/cloud_security_posture/public/components/test_subjects.ts +++ b/x-pack/plugins/cloud_security_posture/public/components/test_subjects.ts @@ -42,8 +42,6 @@ export const THIRD_PARTY_NO_VULNERABILITIES_FINDINGS_PROMPT_WIZ_INTEGRATION_BUTT '3p-no-vulnerabilities-findings-prompt-wiz-integration-button'; export const VULNERABILITIES_CONTAINER_TEST_SUBJ = 'vulnerabilities_container'; -export const VULNERABILITIES_CVSS_SCORE_BADGE_SUBJ = 'vulnerabilities_cvss_score_badge'; - export const TAKE_ACTION_SUBJ = 'csp:take_action'; export const CREATE_RULE_ACTION_SUBJ = 'csp:create_rule'; diff --git a/x-pack/plugins/cloud_security_posture/public/components/vulnerability_severity_map.tsx b/x-pack/plugins/cloud_security_posture/public/components/vulnerability_severity_map.tsx index 9046fcb265a86..4773f5378fb3e 100644 --- a/x-pack/plugins/cloud_security_posture/public/components/vulnerability_severity_map.tsx +++ b/x-pack/plugins/cloud_security_posture/public/components/vulnerability_severity_map.tsx @@ -18,7 +18,7 @@ import { PaletteColorStop } from '@elastic/eui/src/components/color_picker/color import type { VulnSeverity } from '@kbn/cloud-security-posture-common'; import { i18n } from '@kbn/i18n'; import { getSeverityStatusColor } from '@kbn/cloud-security-posture'; -import { SeverityStatusBadge } from './vulnerability_badges'; +import { SeverityStatusBadge } from '@kbn/cloud-security-posture'; interface Props { total: number; diff --git a/x-pack/plugins/cloud_security_posture/public/pages/configurations/latest_findings/use_latest_findings.ts b/x-pack/plugins/cloud_security_posture/public/pages/configurations/latest_findings/use_latest_findings.ts index f6f27e15ee7a4..068eb3df1b10f 100644 --- a/x-pack/plugins/cloud_security_posture/public/pages/configurations/latest_findings/use_latest_findings.ts +++ b/x-pack/plugins/cloud_security_posture/public/pages/configurations/latest_findings/use_latest_findings.ts @@ -15,12 +15,13 @@ import { showErrorToast } from '@kbn/cloud-security-posture'; import { MAX_FINDINGS_TO_LOAD, buildMutedRulesFilter } from '@kbn/cloud-security-posture-common'; import { CDR_MISCONFIGURATIONS_INDEX_PATTERN, - LATEST_FINDINGS_RETENTION_POLICY, + CDR_3RD_PARTY_RETENTION_POLICY, } from '@kbn/cloud-security-posture-common'; import type { CspFinding } from '@kbn/cloud-security-posture-common'; import type { CspBenchmarkRulesStates } from '@kbn/cloud-security-posture-common/schema/rules/latest'; import type { FindingsBaseEsQuery } from '@kbn/cloud-security-posture'; import { useGetCspBenchmarkRulesStatesApi } from '@kbn/cloud-security-posture/src/hooks/use_get_benchmark_rules_state_api'; +import type { RuntimePrimitiveTypes } from '@kbn/data-views-plugin/common'; import { useKibana } from '../../../common/hooks/use_kibana'; import { getAggregationCount, getFindingsCountAggQuery } from '../utils/utils'; @@ -39,6 +40,20 @@ interface FindingsAggs { count: estypes.AggregationsMultiBucketAggregateBase; } +const getRuntimeMappingsFromSort = (sort: string[][]) => { + return sort.reduce((acc, [field]) => { + // TODO: Add proper type for all fields available in the field selector + const type: RuntimePrimitiveTypes = field === '@timestamp' ? 'date' : 'keyword'; + + return { + ...acc, + [field]: { + type, + }, + }; + }, {}); +}; + export const getFindingsQuery = ( { query, sort }: UseFindingsOptions, rulesStates: CspBenchmarkRulesStates, @@ -49,6 +64,7 @@ export const getFindingsQuery = ( return { index: CDR_MISCONFIGURATIONS_INDEX_PATTERN, sort: getMultiFieldsSort(sort), + runtime_mappings: getRuntimeMappingsFromSort(sort), size: MAX_FINDINGS_TO_LOAD, aggs: getFindingsCountAggQuery(), ignore_unavailable: true, @@ -61,7 +77,7 @@ export const getFindingsQuery = ( { range: { '@timestamp': { - gte: `now-${LATEST_FINDINGS_RETENTION_POLICY}`, + gte: `now-${CDR_3RD_PARTY_RETENTION_POLICY}`, lte: 'now', }, }, diff --git a/x-pack/plugins/cloud_security_posture/public/pages/configurations/latest_findings/use_latest_findings_grouping.tsx b/x-pack/plugins/cloud_security_posture/public/pages/configurations/latest_findings/use_latest_findings_grouping.tsx index cc409fb95024d..e009ee966fb96 100644 --- a/x-pack/plugins/cloud_security_posture/public/pages/configurations/latest_findings/use_latest_findings_grouping.tsx +++ b/x-pack/plugins/cloud_security_posture/public/pages/configurations/latest_findings/use_latest_findings_grouping.tsx @@ -16,7 +16,7 @@ import { import { useMemo } from 'react'; import { buildEsQuery, Filter } from '@kbn/es-query'; import { - LATEST_FINDINGS_RETENTION_POLICY, + CDR_3RD_PARTY_RETENTION_POLICY, buildMutedRulesFilter, } from '@kbn/cloud-security-posture-common'; import { useGetCspBenchmarkRulesStatesApi } from '@kbn/cloud-security-posture/src/hooks/use_get_benchmark_rules_state_api'; @@ -114,6 +114,72 @@ const getAggregationsByGroupField = (field: string): NamedAggregation[] => { return aggMetrics; }; +/** + * Get runtime mappings for the given group field + * Some fields require additional runtime mappings to aggregate additional information + * Fallback to keyword type to support custom fields grouping + */ +const getRuntimeMappingsByGroupField = ( + field: string +): Record | undefined => { + switch (field) { + case FINDINGS_GROUPING_OPTIONS.RESOURCE_NAME: + return { + [FINDINGS_GROUPING_OPTIONS.RESOURCE_NAME]: { + type: 'keyword', + }, + 'resource.id': { + type: 'keyword', + }, + 'resource.sub_type': { + type: 'keyword', + }, + 'resource.type': { + type: 'keyword', + }, + }; + case FINDINGS_GROUPING_OPTIONS.RULE_NAME: + return { + [FINDINGS_GROUPING_OPTIONS.RULE_NAME]: { + type: 'keyword', + }, + 'rule.benchmark.version': { + type: 'keyword', + }, + }; + case FINDINGS_GROUPING_OPTIONS.CLOUD_ACCOUNT_NAME: + return { + [FINDINGS_GROUPING_OPTIONS.CLOUD_ACCOUNT_NAME]: { + type: 'keyword', + }, + 'rule.benchmark.name': { + type: 'keyword', + }, + 'rule.benchmark.id': { + type: 'keyword', + }, + }; + case FINDINGS_GROUPING_OPTIONS.ORCHESTRATOR_CLUSTER_NAME: + return { + [FINDINGS_GROUPING_OPTIONS.ORCHESTRATOR_CLUSTER_NAME]: { + type: 'keyword', + }, + 'rule.benchmark.name': { + type: 'keyword', + }, + 'rule.benchmark.id': { + type: 'keyword', + }, + }; + default: + return { + [field]: { + type: 'keyword', + }, + }; + } +}; + /** * Type Guard for checking if the given source is a FindingsRootGroupingAggregation */ @@ -183,12 +249,18 @@ export const useLatestFindingsGrouping = ({ additionalFilters: query ? [query, additionalFilters] : [additionalFilters], groupByField: currentSelectedGroup, uniqueValue, - from: `now-${LATEST_FINDINGS_RETENTION_POLICY}`, + from: `now-${CDR_3RD_PARTY_RETENTION_POLICY}`, to: 'now', pageNumber: activePageIndex * pageSize, size: pageSize, sort: [{ groupByField: { order: 'desc' } }, { complianceScore: { order: 'asc' } }], statsAggregations: getAggregationsByGroupField(currentSelectedGroup), + runtimeMappings: { + ...getRuntimeMappingsByGroupField(currentSelectedGroup), + 'result.evaluation': { + type: 'keyword', + }, + }, rootAggregations: [ { failedFindings: { diff --git a/x-pack/plugins/cloud_security_posture/public/pages/vulnerabilities/hooks/use_latest_vulnerabilities.tsx b/x-pack/plugins/cloud_security_posture/public/pages/vulnerabilities/hooks/use_latest_vulnerabilities.tsx index 0d0ea9ba5a22f..5f01a4693c8f5 100644 --- a/x-pack/plugins/cloud_security_posture/public/pages/vulnerabilities/hooks/use_latest_vulnerabilities.tsx +++ b/x-pack/plugins/cloud_security_posture/public/pages/vulnerabilities/hooks/use_latest_vulnerabilities.tsx @@ -19,10 +19,11 @@ import { EsHitRecord } from '@kbn/discover-utils/types'; import { MAX_FINDINGS_TO_LOAD, CDR_VULNERABILITIES_INDEX_PATTERN, - LATEST_VULNERABILITIES_RETENTION_POLICY, + CDR_3RD_PARTY_RETENTION_POLICY, } from '@kbn/cloud-security-posture-common'; import { FindingsBaseEsQuery, showErrorToast } from '@kbn/cloud-security-posture'; import type { CspVulnerabilityFinding } from '@kbn/cloud-security-posture-common/schema/vulnerabilities/latest'; +import type { RuntimePrimitiveTypes } from '@kbn/data-views-plugin/common'; import { VULNERABILITY_FIELDS } from '../../../common/constants'; import { useKibana } from '../../../common/hooks/use_kibana'; import { getCaseInsensitiveSortScript } from '../utils/custom_sort_script'; @@ -52,6 +53,25 @@ const getMultiFieldsSort = (sort: string[][]) => { }); }; +const getRuntimeMappingsFromSort = (sort: string[][]) => { + return sort.reduce((acc, [field]) => { + // TODO: Add proper type for all fields available in the field selector + const type: RuntimePrimitiveTypes = + field === VULNERABILITY_FIELDS.SCORE_BASE + ? 'double' + : field === '@timestamp' + ? 'date' + : 'keyword'; + + return { + ...acc, + [field]: { + type, + }, + }; + }, {}); +}; + export const getVulnerabilitiesQuery = ( { query, sort }: VulnerabilitiesQuery, pageParam: number @@ -59,6 +79,7 @@ export const getVulnerabilitiesQuery = ( index: CDR_VULNERABILITIES_INDEX_PATTERN, ignore_unavailable: true, sort: getMultiFieldsSort(sort), + runtime_mappings: getRuntimeMappingsFromSort(sort), size: MAX_FINDINGS_TO_LOAD, query: { ...query, @@ -69,7 +90,7 @@ export const getVulnerabilitiesQuery = ( { range: { '@timestamp': { - gte: `now-${LATEST_VULNERABILITIES_RETENTION_POLICY}`, + gte: `now-${CDR_3RD_PARTY_RETENTION_POLICY}`, lte: 'now', }, }, diff --git a/x-pack/plugins/cloud_security_posture/public/pages/vulnerabilities/hooks/use_latest_vulnerabilities_grouping.tsx b/x-pack/plugins/cloud_security_posture/public/pages/vulnerabilities/hooks/use_latest_vulnerabilities_grouping.tsx index 516cbed0c3975..d79b4620ec899 100644 --- a/x-pack/plugins/cloud_security_posture/public/pages/vulnerabilities/hooks/use_latest_vulnerabilities_grouping.tsx +++ b/x-pack/plugins/cloud_security_posture/public/pages/vulnerabilities/hooks/use_latest_vulnerabilities_grouping.tsx @@ -15,7 +15,7 @@ import { } from '@kbn/grouping/src'; import { useMemo } from 'react'; import { - LATEST_VULNERABILITIES_RETENTION_POLICY, + CDR_3RD_PARTY_RETENTION_POLICY, VULNERABILITIES_SEVERITY, } from '@kbn/cloud-security-posture-common'; import { buildEsQuery, Filter } from '@kbn/es-query'; @@ -94,6 +94,51 @@ const getAggregationsByGroupField = (field: string): NamedAggregation[] => { return aggMetrics; }; +/** + * Get runtime mappings for the given group field + * Some fields require additional runtime mappings to aggregate additional information + * Fallback to keyword type to support custom fields grouping + */ +const getRuntimeMappingsByGroupField = ( + field: string +): Record | undefined => { + switch (field) { + case VULNERABILITY_GROUPING_OPTIONS.CLOUD_ACCOUNT_NAME: + return { + [VULNERABILITY_GROUPING_OPTIONS.CLOUD_ACCOUNT_NAME]: { + type: 'keyword', + }, + [VULNERABILITY_FIELDS.CLOUD_PROVIDER]: { + type: 'keyword', + }, + }; + case VULNERABILITY_GROUPING_OPTIONS.RESOURCE_NAME: + return { + [VULNERABILITY_GROUPING_OPTIONS.RESOURCE_NAME]: { + type: 'keyword', + }, + [VULNERABILITY_FIELDS.RESOURCE_ID]: { + type: 'keyword', + }, + }; + case VULNERABILITY_GROUPING_OPTIONS.CVE: + return { + [VULNERABILITY_GROUPING_OPTIONS.CVE]: { + type: 'keyword', + }, + [VULNERABILITY_FIELDS.DESCRIPTION]: { + type: 'keyword', + }, + }; + default: + return { + [field]: { + type: 'keyword', + }, + }; + } +}; + /** * Type Guard for checking if the given source is a VulnerabilitiesRootGroupingAggregation */ @@ -157,12 +202,13 @@ export const useLatestVulnerabilitiesGrouping = ({ additionalFilters: query ? [query, additionalFilters] : [additionalFilters], groupByField: currentSelectedGroup, uniqueValue, - from: `now-${LATEST_VULNERABILITIES_RETENTION_POLICY}`, + from: `now-${CDR_3RD_PARTY_RETENTION_POLICY}`, to: 'now', pageNumber: activePageIndex * pageSize, size: pageSize, sort: [{ groupByField: { order: 'desc' } }], statsAggregations: getAggregationsByGroupField(currentSelectedGroup), + runtimeMappings: getRuntimeMappingsByGroupField(currentSelectedGroup), }); const { data, isFetching } = useGroupedVulnerabilities({ diff --git a/x-pack/plugins/cloud_security_posture/public/pages/vulnerabilities/latest_vulnerabilities_table.tsx b/x-pack/plugins/cloud_security_posture/public/pages/vulnerabilities/latest_vulnerabilities_table.tsx index 09c2989c1eb23..8b485d0b7e042 100644 --- a/x-pack/plugins/cloud_security_posture/public/pages/vulnerabilities/latest_vulnerabilities_table.tsx +++ b/x-pack/plugins/cloud_security_posture/public/pages/vulnerabilities/latest_vulnerabilities_table.tsx @@ -12,6 +12,7 @@ import { EuiDataGridCellValueElementProps, EuiSpacer } from '@elastic/eui'; import { Filter } from '@kbn/es-query'; import { HttpSetup } from '@kbn/core-http-browser'; import type { CspVulnerabilityFinding } from '@kbn/cloud-security-posture-common/schema/vulnerabilities/latest'; +import { CVSScoreBadge, SeverityStatusBadge } from '@kbn/cloud-security-posture'; import { getVendorName } from '../../common/utils/get_vendor_name'; import { CloudSecurityDataTable } from '../../components/cloud_security_data_table'; import { useLatestVulnerabilitiesTable } from './hooks/use_latest_vulnerabilities_table'; @@ -19,7 +20,6 @@ import { LATEST_VULNERABILITIES_TABLE } from './test_subjects'; import { getDefaultQuery, defaultColumns } from './constants'; import { VulnerabilityFindingFlyout } from './vulnerabilities_finding_flyout/vulnerability_finding_flyout'; import { ErrorCallout } from '../configurations/layout/error_callout'; -import { CVSScoreBadge, SeverityStatusBadge } from '../../components/vulnerability_badges'; import { createDetectionRuleFromVulnerabilityFinding } from './utils/create_detection_rule_from_vulnerability'; import { vulnerabilitiesTableFieldLabels } from './vulnerabilities_table_field_labels'; @@ -108,11 +108,11 @@ export const LatestVulnerabilitiesTable = ({ }); const createVulnerabilityRuleFn = (rowIndex: number) => { - const finding = getCspVulnerabilityFinding(rows[rowIndex].raw._source); - if (!finding) return; + const vulnerabilityFinding = getCspVulnerabilityFinding(rows[rowIndex].raw._source); + if (!vulnerabilityFinding) return; return async (http: HttpSetup) => - createDetectionRuleFromVulnerabilityFinding(http, finding.vulnerability); + createDetectionRuleFromVulnerabilityFinding(http, vulnerabilityFinding); }; return ( diff --git a/x-pack/plugins/cloud_security_posture/public/pages/vulnerabilities/utils/create_detection_rule_from_vulnerability.test.ts b/x-pack/plugins/cloud_security_posture/public/pages/vulnerabilities/utils/create_detection_rule_from_vulnerability.test.ts new file mode 100644 index 0000000000000..7dd0982cc58b5 --- /dev/null +++ b/x-pack/plugins/cloud_security_posture/public/pages/vulnerabilities/utils/create_detection_rule_from_vulnerability.test.ts @@ -0,0 +1,98 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { + getVulnerabilityTags, + getVulnerabilityRuleName, + generateVulnerabilitiesRuleQuery, +} from './create_detection_rule_from_vulnerability'; +import { CspVulnerabilityFinding, Vulnerability } from '@kbn/cloud-security-posture-common'; +import { isNativeCspFinding } from '../../../common/utils/is_native_csp_finding'; + +// Mocking the isNativeCspFinding function +jest.mock('../../../common/utils/is_native_csp_finding', () => ({ + isNativeCspFinding: jest.fn(), +})); + +describe('CreateDetectionRuleFromVulnerability', () => { + describe('getVulnerabilityTags', () => { + it('should return tags with CSP_RULE_TAG and vulnerability id', () => { + const mockVulnerability = { + vulnerability: { id: 'CVE-2024-00001' }, + observer: undefined, + data_stream: undefined, + } as unknown as CspVulnerabilityFinding; + + (isNativeCspFinding as jest.Mock).mockReturnValue(false); + + const tags = getVulnerabilityTags(mockVulnerability); + expect(tags).toEqual(['Cloud Security', 'CVE-2024-00001']); + }); + + it('should include vendor tag if available', () => { + const mockVulnerability = { + vulnerability: { id: 'CVE-2024-00002' }, + observer: { vendor: 'Wiz' }, + data_stream: undefined, + } as unknown as CspVulnerabilityFinding; + + (isNativeCspFinding as jest.Mock).mockReturnValue(false); + + const tags = getVulnerabilityTags(mockVulnerability); + expect(tags).toEqual(['Cloud Security', 'CVE-2024-00002', 'Wiz']); + }); + + it('should include CNVM tags for native findings', () => { + const mockVulnerability = { + vulnerability: { id: 'CVE-2024-00003' }, + observer: undefined, + data_stream: undefined, + } as unknown as CspVulnerabilityFinding; + + (isNativeCspFinding as jest.Mock).mockReturnValue(true); + + const tags = getVulnerabilityTags(mockVulnerability); + expect(tags).toEqual([ + 'Cloud Security', + 'CNVM', + 'Data Source: Cloud Native Vulnerability Management', + 'Use Case: Vulnerability', + 'OS: Linux', + 'CVE-2024-00003', + ]); + }); + }); + + describe('getVulnerabilityRuleName', () => { + it('should return correct rule name for a vulnerability', () => { + const mockVulnerability = { + id: 'CVE-2024-00004', + description: '', + reference: '', + } as Vulnerability; + + const ruleName = getVulnerabilityRuleName(mockVulnerability); + expect(ruleName).toEqual('Vulnerability: CVE-2024-00004'); + }); + }); + + describe('generateVulnerabilitiesRuleQuery', () => { + it('should generate correct query for a vulnerability', () => { + const mockVulnerability = { + id: 'CVE-2024-00005', + description: '', + reference: '', + } as Vulnerability; + const currentTimestamp = new Date().toISOString(); + + const query = generateVulnerabilitiesRuleQuery(mockVulnerability, currentTimestamp); + expect(query).toEqual( + `vulnerability.id: "CVE-2024-00005" AND event.ingested >= "${currentTimestamp}"` + ); + }); + }); +}); diff --git a/x-pack/plugins/cloud_security_posture/public/pages/vulnerabilities/utils/create_detection_rule_from_vulnerability.ts b/x-pack/plugins/cloud_security_posture/public/pages/vulnerabilities/utils/create_detection_rule_from_vulnerability.ts index a09f9130836b2..804e89fad61d8 100644 --- a/x-pack/plugins/cloud_security_posture/public/pages/vulnerabilities/utils/create_detection_rule_from_vulnerability.ts +++ b/x-pack/plugins/cloud_security_posture/public/pages/vulnerabilities/utils/create_detection_rule_from_vulnerability.ts @@ -8,10 +8,12 @@ import { HttpSetup } from '@kbn/core/public'; import { i18n } from '@kbn/i18n'; import { + CspVulnerabilityFinding, LATEST_VULNERABILITIES_RETENTION_POLICY, VULNERABILITIES_SEVERITY, } from '@kbn/cloud-security-posture-common'; import type { Vulnerability } from '@kbn/cloud-security-posture-common/schema/vulnerabilities/latest'; +import { isNativeCspFinding } from '../../../common/utils/is_native_csp_finding'; import { VULNERABILITIES_INDEX_PATTERN } from '../../../../common/constants'; import { createDetectionRule } from '../../../common/api/create_detection_rule'; @@ -42,15 +44,7 @@ enum AlertSuppressionMissingFieldsStrategy { Suppress = 'suppress', } -const CSP_RULE_TAG = 'Cloud Security'; - -const STATIC_RULE_TAGS = [CSP_RULE_TAG]; - -const generateVulnerabilitiesTags = (tags?: string[]) => { - return [...STATIC_RULE_TAGS, ...(!!tags?.length ? tags : [])]; -}; - -const getVulnerabilityRuleName = (vulnerability: Vulnerability) => { +export const getVulnerabilityRuleName = (vulnerability: Vulnerability) => { return i18n.translate('xpack.csp.vulnerabilities.detectionRuleNamePrefix', { defaultMessage: 'Vulnerability: {vulnerabilityId}', values: { @@ -59,10 +53,31 @@ const getVulnerabilityRuleName = (vulnerability: Vulnerability) => { }); }; -const generateVulnerabilitiesRuleQuery = (vulnerability: Vulnerability) => { - const currentTimestamp = new Date().toISOString(); +export const generateVulnerabilitiesRuleQuery = ( + vulnerability: Vulnerability, + startTimestamp = new Date().toISOString() +) => { + return `vulnerability.id: "${vulnerability.id}" AND event.ingested >= "${startTimestamp}"`; +}; + +const CSP_RULE_TAG = 'Cloud Security'; +const CNVM_TAG = 'CNVM'; +const CNVM_RULE_TAG_DATA_SOURCE = 'Data Source: Cloud Native Vulnerability Management'; +const CNVM_RULE_TAG_USE_CASE = 'Use Case: Vulnerability'; +const CNVM_RULE_TAG_OS = 'OS: Linux'; + +export const getVulnerabilityTags = (vulnerabilityFinding: CspVulnerabilityFinding) => { + let tags = [vulnerabilityFinding.vulnerability.id]; + const vendor = + vulnerabilityFinding.observer?.vendor || vulnerabilityFinding?.data_stream?.dataset; - return `vulnerability.id: "${vulnerability.id}" AND event.ingested >= "${currentTimestamp}"`; + if (isNativeCspFinding(vulnerabilityFinding)) { + tags = [CNVM_TAG, CNVM_RULE_TAG_DATA_SOURCE, CNVM_RULE_TAG_USE_CASE, CNVM_RULE_TAG_OS, ...tags]; + } else if (!!vendor) { + tags.push(vendor); + } + + return [CSP_RULE_TAG, ...tags]; }; /* @@ -70,9 +85,11 @@ const generateVulnerabilitiesRuleQuery = (vulnerability: Vulnerability) => { */ export const createDetectionRuleFromVulnerabilityFinding = async ( http: HttpSetup, - vulnerability: Vulnerability, - tags?: string[] + vulnerabilityFinding: CspVulnerabilityFinding ) => { + const tags = getVulnerabilityTags(vulnerabilityFinding); + const vulnerability = vulnerabilityFinding.vulnerability; + return await createDetectionRule({ http, rule: { @@ -135,7 +152,7 @@ export const createDetectionRuleFromVulnerabilityFinding = async ( references: vulnerability.reference ? [vulnerability.reference] : [], name: getVulnerabilityRuleName(vulnerability), description: vulnerability.description, - tags: generateVulnerabilitiesTags(tags), + tags, investigation_fields: DEFAULT_INVESTIGATION_FIELDS, }, }); diff --git a/x-pack/plugins/cloud_security_posture/public/pages/vulnerabilities/utils/custom_sort_script.ts b/x-pack/plugins/cloud_security_posture/public/pages/vulnerabilities/utils/custom_sort_script.ts index 780cd539305b3..e517d622e71c5 100644 --- a/x-pack/plugins/cloud_security_posture/public/pages/vulnerabilities/utils/custom_sort_script.ts +++ b/x-pack/plugins/cloud_security_posture/public/pages/vulnerabilities/utils/custom_sort_script.ts @@ -14,7 +14,13 @@ export const getCaseInsensitiveSortScript = (field: string, direction: string) = type: 'string', order: direction, script: { - source: `doc["${field}"].value.toLowerCase()`, + source: ` + if (doc.containsKey('${field}') && !doc['${field}'].empty) { + return doc['${field}'].value.toLowerCase(); + } else { + return ""; + } + `, lang: 'painless', }, }, diff --git a/x-pack/plugins/cloud_security_posture/public/pages/vulnerabilities/vulnerabilities_finding_flyout/vulnerability_detection_rule_counter.tsx b/x-pack/plugins/cloud_security_posture/public/pages/vulnerabilities/vulnerabilities_finding_flyout/vulnerability_detection_rule_counter.tsx index 1c726a450655b..c4680177f72c0 100644 --- a/x-pack/plugins/cloud_security_posture/public/pages/vulnerabilities/vulnerabilities_finding_flyout/vulnerability_detection_rule_counter.tsx +++ b/x-pack/plugins/cloud_security_posture/public/pages/vulnerabilities/vulnerabilities_finding_flyout/vulnerability_detection_rule_counter.tsx @@ -8,40 +8,21 @@ import React from 'react'; import type { HttpSetup } from '@kbn/core/public'; import type { CspVulnerabilityFinding } from '@kbn/cloud-security-posture-common/schema/vulnerabilities/latest'; -import { isNativeCspFinding } from '../../../common/utils/is_native_csp_finding'; import { DetectionRuleCounter } from '../../../components/detection_rule_counter'; -import { createDetectionRuleFromVulnerabilityFinding } from '../utils/create_detection_rule_from_vulnerability'; - -const CNVM_TAG = 'CNVM'; -const CNVM_RULE_TAG_DATA_SOURCE = 'Data Source: Cloud Native Vulnerability Management'; -const CNVM_RULE_TAG_USE_CASE = 'Use Case: Vulnerability'; -const CNVM_RULE_TAG_OS = 'OS: Linux'; - -const getTags = (vulnerabilityRecord: CspVulnerabilityFinding) => { - let tags = [vulnerabilityRecord.vulnerability.id]; - const vendor = vulnerabilityRecord.observer?.vendor || vulnerabilityRecord?.data_stream?.dataset; - - if (isNativeCspFinding(vulnerabilityRecord)) { - tags = [CNVM_TAG, CNVM_RULE_TAG_DATA_SOURCE, CNVM_RULE_TAG_USE_CASE, CNVM_RULE_TAG_OS, ...tags]; - } else if (!!vendor) { - tags.push(vendor); - } - - return tags; -}; +import { + createDetectionRuleFromVulnerabilityFinding, + getVulnerabilityTags, +} from '../utils/create_detection_rule_from_vulnerability'; export const VulnerabilityDetectionRuleCounter = ({ vulnerabilityRecord, }: { vulnerabilityRecord: CspVulnerabilityFinding; }) => { - const tags = getTags(vulnerabilityRecord); + const tags = getVulnerabilityTags(vulnerabilityRecord); + const createVulnerabilityRuleFn = async (http: HttpSetup) => - await createDetectionRuleFromVulnerabilityFinding( - http, - vulnerabilityRecord.vulnerability, - tags - ); + await createDetectionRuleFromVulnerabilityFinding(http, vulnerabilityRecord); return ; }; diff --git a/x-pack/plugins/cloud_security_posture/public/pages/vulnerabilities/vulnerabilities_finding_flyout/vulnerability_finding_flyout.tsx b/x-pack/plugins/cloud_security_posture/public/pages/vulnerabilities/vulnerabilities_finding_flyout/vulnerability_finding_flyout.tsx index 8c7e3341424d9..392059c8cc0f4 100644 --- a/x-pack/plugins/cloud_security_posture/public/pages/vulnerabilities/vulnerabilities_finding_flyout/vulnerability_finding_flyout.tsx +++ b/x-pack/plugins/cloud_security_posture/public/pages/vulnerabilities/vulnerabilities_finding_flyout/vulnerability_finding_flyout.tsx @@ -28,13 +28,13 @@ import { euiThemeVars } from '@kbn/ui-theme'; import { css } from '@emotion/react'; import { HttpSetup } from '@kbn/core-http-browser'; import type { CspVulnerabilityFinding } from '@kbn/cloud-security-posture-common/schema/vulnerabilities/latest'; +import { SeverityStatusBadge } from '@kbn/cloud-security-posture'; import { isNativeCspFinding } from '../../../common/utils/is_native_csp_finding'; import { TakeAction } from '../../../components/take_action'; import { truthy } from '../../../../common/utils/helpers'; import { CspInlineDescriptionList } from '../../../components/csp_inline_description_list'; import { VulnerabilityOverviewTab } from './vulnerability_overview_tab'; import { VulnerabilityJsonTab } from './vulnerability_json_tab'; -import { SeverityStatusBadge } from '../../../components/vulnerability_badges'; import { FINDINGS_VULNERABILITY_FLYOUT_DESCRIPTION_LIST, TAB_ID_VULNERABILITY_FLYOUT, @@ -162,7 +162,7 @@ export const VulnerabilityFindingFlyout = ({ const vulnerabilityReference = vulnerability?.reference; const createVulnerabilityRuleFn = async (http: HttpSetup) => - await createDetectionRuleFromVulnerabilityFinding(http, vulnerabilityRecord.vulnerability); + await createDetectionRuleFromVulnerabilityFinding(http, vulnerabilityRecord); return ( diff --git a/x-pack/plugins/cloud_security_posture/public/pages/vulnerabilities/vulnerabilities_finding_flyout/vulnerability_overview_tab.tsx b/x-pack/plugins/cloud_security_posture/public/pages/vulnerabilities/vulnerabilities_finding_flyout/vulnerability_overview_tab.tsx index d67649c508c13..ed9d7f985f357 100644 --- a/x-pack/plugins/cloud_security_posture/public/pages/vulnerabilities/vulnerabilities_finding_flyout/vulnerability_overview_tab.tsx +++ b/x-pack/plugins/cloud_security_posture/public/pages/vulnerabilities/vulnerabilities_finding_flyout/vulnerability_overview_tab.tsx @@ -28,10 +28,10 @@ import { VULNERABILITIES_FLYOUT_VISITS, uiMetricService, } from '@kbn/cloud-security-posture-common/utils/ui_metrics'; +import { CVSScoreBadge } from '@kbn/cloud-security-posture'; import { getVendorName } from '../../../common/utils/get_vendor_name'; import { CspFlyoutMarkdown } from '../../configurations/findings_flyout/findings_flyout'; import { NvdLogo } from '../../../assets/icons/nvd_logo_svg'; -import { CVSScoreBadge } from '../../../components/vulnerability_badges'; import { CVSScoreProps, Vendor } from '../types'; import { getVectorScoreList } from '../utils/get_vector_score_list'; import { diff --git a/x-pack/plugins/cloud_security_posture/public/pages/vulnerability_dashboard/vulnerability_table_panel_section.tsx b/x-pack/plugins/cloud_security_posture/public/pages/vulnerability_dashboard/vulnerability_table_panel_section.tsx index 42794e91d2036..28012e3e8e438 100644 --- a/x-pack/plugins/cloud_security_posture/public/pages/vulnerability_dashboard/vulnerability_table_panel_section.tsx +++ b/x-pack/plugins/cloud_security_posture/public/pages/vulnerability_dashboard/vulnerability_table_panel_section.tsx @@ -19,6 +19,7 @@ import { i18n } from '@kbn/i18n'; import type { NavFilter } from '@kbn/cloud-security-posture/src/hooks/use_navigate_findings'; import { useNavigateVulnerabilities } from '@kbn/cloud-security-posture/src/hooks/use_navigate_findings'; import type { VulnSeverity } from '@kbn/cloud-security-posture-common'; +import { CVSScoreBadge, SeverityStatusBadge } from '@kbn/cloud-security-posture'; import { PatchableVulnerabilityStat, VulnerabilityStat, @@ -26,7 +27,6 @@ import { } from '../../../common/types_old'; import { DASHBOARD_TABLE_TYPES } from './vulnerability_table_panel.config'; import { VulnerabilityTablePanel } from './vulnerability_table_panel'; -import { CVSScoreBadge, SeverityStatusBadge } from '../../components/vulnerability_badges'; import { useVulnerabilityDashboardApi } from '../../common/api/use_vulnerability_dashboard_api'; import { VULNERABILITY_GROUPING_OPTIONS, VULNERABILITY_FIELDS } from '../../common/constants'; diff --git a/x-pack/plugins/cloud_security_posture/server/routes/status/status.ts b/x-pack/plugins/cloud_security_posture/server/routes/status/status.ts index 659b2ed94f43a..4f5c84b936fb2 100644 --- a/x-pack/plugins/cloud_security_posture/server/routes/status/status.ts +++ b/x-pack/plugins/cloud_security_posture/server/routes/status/status.ts @@ -15,6 +15,7 @@ import { CDR_LATEST_NATIVE_VULNERABILITIES_INDEX_PATTERN, LATEST_VULNERABILITIES_RETENTION_POLICY, CDR_VULNERABILITIES_INDEX_PATTERN, + CDR_3RD_PARTY_RETENTION_POLICY, } from '@kbn/cloud-security-posture-common'; import type { CspSetupStatus, @@ -218,13 +219,13 @@ export const getCspStatus = async ({ checkIndexHasFindings( esClient, CDR_MISCONFIGURATIONS_INDEX_PATTERN, - LATEST_FINDINGS_RETENTION_POLICY, + CDR_3RD_PARTY_RETENTION_POLICY, logger ), checkIndexHasFindings( esClient, CDR_VULNERABILITIES_INDEX_PATTERN, - LATEST_VULNERABILITIES_RETENTION_POLICY, + CDR_3RD_PARTY_RETENTION_POLICY, logger ), checkIndexStatus(esClient, LATEST_FINDINGS_INDEX_DEFAULT_NS, logger, { diff --git a/x-pack/plugins/cross_cluster_replication/public/app/sections/home/follower_indices_list/components/detail_panel/detail_panel.js b/x-pack/plugins/cross_cluster_replication/public/app/sections/home/follower_indices_list/components/detail_panel/detail_panel.js index 351f1bd77592f..c0b6c0f4c9a09 100644 --- a/x-pack/plugins/cross_cluster_replication/public/app/sections/home/follower_indices_list/components/detail_panel/detail_panel.js +++ b/x-pack/plugins/cross_cluster_replication/public/app/sections/home/follower_indices_list/components/detail_panel/detail_panel.js @@ -30,8 +30,6 @@ import { EuiTextColor, EuiTitle, } from '@elastic/eui'; -import 'react-ace'; -import 'brace/theme/textmate'; import { getIndexListUri } from '@kbn/index-management-plugin/public'; import { routing } from '../../../../../services/routing'; diff --git a/x-pack/plugins/data_usage/common/rest_types/usage_metrics.test.ts b/x-pack/plugins/data_usage/common/rest_types/usage_metrics.test.ts index f6c08e2caddc0..473e64c6b03d9 100644 --- a/x-pack/plugins/data_usage/common/rest_types/usage_metrics.test.ts +++ b/x-pack/plugins/data_usage/common/rest_types/usage_metrics.test.ts @@ -10,48 +10,29 @@ import { UsageMetricsRequestSchema } from './usage_metrics'; describe('usage_metrics schemas', () => { it('should accept valid request query', () => { expect(() => - UsageMetricsRequestSchema.query.validate({ + UsageMetricsRequestSchema.validate({ from: new Date().toISOString(), to: new Date().toISOString(), metricTypes: ['storage_retained'], - }) - ).not.toThrow(); - }); - - it('should accept a single `metricTypes` in request query', () => { - expect(() => - UsageMetricsRequestSchema.query.validate({ - from: new Date().toISOString(), - to: new Date().toISOString(), - metricTypes: 'ingest_rate', + dataStreams: ['data_stream_1', 'data_stream_2', 'data_stream_3'], }) ).not.toThrow(); }); it('should accept multiple `metricTypes` in request query', () => { expect(() => - UsageMetricsRequestSchema.query.validate({ + UsageMetricsRequestSchema.validate({ from: new Date().toISOString(), to: new Date().toISOString(), metricTypes: ['ingest_rate', 'storage_retained', 'index_rate'], - }) - ).not.toThrow(); - }); - - it('should accept a single string as `dataStreams` in request query', () => { - expect(() => - UsageMetricsRequestSchema.query.validate({ - from: new Date().toISOString(), - to: new Date().toISOString(), - metricTypes: 'storage_retained', - dataStreams: 'data_stream_1', + dataStreams: ['data_stream_1', 'data_stream_2', 'data_stream_3'], }) ).not.toThrow(); }); it('should accept `dataStream` list', () => { expect(() => - UsageMetricsRequestSchema.query.validate({ + UsageMetricsRequestSchema.validate({ from: new Date().toISOString(), to: new Date().toISOString(), metricTypes: ['storage_retained'], @@ -62,74 +43,76 @@ describe('usage_metrics schemas', () => { it('should error if `dataStream` list is empty', () => { expect(() => - UsageMetricsRequestSchema.query.validate({ + UsageMetricsRequestSchema.validate({ from: new Date().toISOString(), to: new Date().toISOString(), metricTypes: ['storage_retained'], dataStreams: [], }) - ).toThrowError('expected value of type [string] but got [Array]'); + ).toThrowError('[dataStreams]: array size is [0], but cannot be smaller than [1]'); }); - it('should error if `dataStream` is given an empty string', () => { + it('should error if `dataStream` is given type not array', () => { expect(() => - UsageMetricsRequestSchema.query.validate({ + UsageMetricsRequestSchema.validate({ from: new Date().toISOString(), to: new Date().toISOString(), metricTypes: ['storage_retained'], dataStreams: ' ', }) - ).toThrow('[dataStreams] must have at least one value'); + ).toThrow('[dataStreams]: could not parse array value from json input'); }); it('should error if `dataStream` is given an empty item in the list', () => { expect(() => - UsageMetricsRequestSchema.query.validate({ + UsageMetricsRequestSchema.validate({ from: new Date().toISOString(), to: new Date().toISOString(), metricTypes: ['storage_retained'], dataStreams: ['ds_1', ' '], }) - ).toThrow('[dataStreams] list can not contain empty values'); + ).toThrow('[dataStreams]: [dataStreams] list cannot contain empty values'); }); it('should error if `metricTypes` is empty string', () => { expect(() => - UsageMetricsRequestSchema.query.validate({ + UsageMetricsRequestSchema.validate({ from: new Date().toISOString(), to: new Date().toISOString(), + dataStreams: ['data_stream_1', 'data_stream_2', 'data_stream_3'], metricTypes: ' ', }) ).toThrow(); }); - it('should error if `metricTypes` is empty item', () => { + it('should error if `metricTypes` contains an empty item', () => { expect(() => - UsageMetricsRequestSchema.query.validate({ + UsageMetricsRequestSchema.validate({ from: new Date().toISOString(), to: new Date().toISOString(), - metricTypes: [' ', 'storage_retained'], + dataStreams: ['data_stream_1', 'data_stream_2', 'data_stream_3'], + metricTypes: [' ', 'storage_retained'], // First item is invalid }) - ).toThrow('[metricTypes] list can not contain empty values'); + ).toThrowError(/list cannot contain empty values/); }); - it('should error if `metricTypes` is not a valid value', () => { + it('should error if `metricTypes` is not a valid type', () => { expect(() => - UsageMetricsRequestSchema.query.validate({ + UsageMetricsRequestSchema.validate({ from: new Date().toISOString(), to: new Date().toISOString(), + dataStreams: ['data_stream_1', 'data_stream_2', 'data_stream_3'], metricTypes: 'foo', }) - ).toThrow( - '[metricTypes] must be one of storage_retained, ingest_rate, search_vcu, ingest_vcu, ml_vcu, index_latency, index_rate, search_latency, search_rate' - ); + ).toThrow('[metricTypes]: could not parse array value from json input'); }); it('should error if `metricTypes` is not a valid list', () => { expect(() => - UsageMetricsRequestSchema.query.validate({ + UsageMetricsRequestSchema.validate({ from: new Date().toISOString(), to: new Date().toISOString(), + dataStreams: ['data_stream_1', 'data_stream_2', 'data_stream_3'], metricTypes: ['storage_retained', 'foo'], }) ).toThrow( @@ -139,9 +122,10 @@ describe('usage_metrics schemas', () => { it('should error if `from` is not a valid input', () => { expect(() => - UsageMetricsRequestSchema.query.validate({ + UsageMetricsRequestSchema.validate({ from: 1010, to: new Date().toISOString(), + dataStreams: ['data_stream_1', 'data_stream_2', 'data_stream_3'], metricTypes: ['storage_retained', 'foo'], }) ).toThrow('[from]: expected value of type [string] but got [number]'); @@ -149,9 +133,10 @@ describe('usage_metrics schemas', () => { it('should error if `to` is not a valid input', () => { expect(() => - UsageMetricsRequestSchema.query.validate({ + UsageMetricsRequestSchema.validate({ from: new Date().toISOString(), to: 1010, + dataStreams: ['data_stream_1', 'data_stream_2', 'data_stream_3'], metricTypes: ['storage_retained', 'foo'], }) ).toThrow('[to]: expected value of type [string] but got [number]'); @@ -159,9 +144,10 @@ describe('usage_metrics schemas', () => { it('should error if `from` is empty string', () => { expect(() => - UsageMetricsRequestSchema.query.validate({ + UsageMetricsRequestSchema.validate({ from: ' ', to: new Date().toISOString(), + dataStreams: ['data_stream_1', 'data_stream_2', 'data_stream_3'], metricTypes: ['storage_retained', 'foo'], }) ).toThrow('[from]: Date ISO string must not be empty'); @@ -169,9 +155,10 @@ describe('usage_metrics schemas', () => { it('should error if `to` is empty string', () => { expect(() => - UsageMetricsRequestSchema.query.validate({ + UsageMetricsRequestSchema.validate({ from: new Date().toISOString(), to: ' ', + dataStreams: ['data_stream_1', 'data_stream_2', 'data_stream_3'], metricTypes: ['storage_retained', 'foo'], }) ).toThrow('[to]: Date ISO string must not be empty'); diff --git a/x-pack/plugins/data_usage/common/rest_types/usage_metrics.ts b/x-pack/plugins/data_usage/common/rest_types/usage_metrics.ts index f2bbdb616fc79..3dceeadc198b0 100644 --- a/x-pack/plugins/data_usage/common/rest_types/usage_metrics.ts +++ b/x-pack/plugins/data_usage/common/rest_types/usage_metrics.ts @@ -37,51 +37,31 @@ const metricTypesSchema = schema.oneOf( // @ts-expect-error TS2769: No overload matches this call METRIC_TYPE_VALUES.map((metricType) => schema.literal(metricType)) // Create a oneOf schema for the keys ); -export const UsageMetricsRequestSchema = { - query: schema.object({ - from: DateSchema, - to: DateSchema, - metricTypes: schema.oneOf([ - schema.arrayOf(schema.string(), { - minSize: 1, - validate: (values) => { - if (values.map((v) => v.trim()).some((v) => !v.length)) { - return '[metricTypes] list can not contain empty values'; - } else if (values.map((v) => v.trim()).some((v) => !isValidMetricType(v))) { - return `[metricTypes] must be one of ${METRIC_TYPE_VALUES.join(', ')}`; - } - }, - }), - schema.string({ - validate: (v) => { - if (!v.trim().length) { - return '[metricTypes] must have at least one value'; - } else if (!isValidMetricType(v)) { - return `[metricTypes] must be one of ${METRIC_TYPE_VALUES.join(', ')}`; - } - }, - }), - ]), - dataStreams: schema.maybe( - schema.oneOf([ - schema.arrayOf(schema.string(), { - minSize: 1, - validate: (values) => { - if (values.map((v) => v.trim()).some((v) => !v.length)) { - return '[dataStreams] list can not contain empty values'; - } - }, - }), - schema.string({ - validate: (v) => - v.trim().length ? undefined : '[dataStreams] must have at least one value', - }), - ]) - ), +export const UsageMetricsRequestSchema = schema.object({ + from: DateSchema, + to: DateSchema, + metricTypes: schema.arrayOf(schema.string(), { + minSize: 1, + validate: (values) => { + const trimmedValues = values.map((v) => v.trim()); + if (trimmedValues.some((v) => !v.length)) { + return '[metricTypes] list cannot contain empty values'; + } else if (trimmedValues.some((v) => !isValidMetricType(v))) { + return `[metricTypes] must be one of ${METRIC_TYPE_VALUES.join(', ')}`; + } + }, }), -}; + dataStreams: schema.arrayOf(schema.string(), { + minSize: 1, + validate: (values) => { + if (values.map((v) => v.trim()).some((v) => !v.length)) { + return '[dataStreams] list cannot contain empty values'; + } + }, + }), +}); -export type UsageMetricsRequestSchemaQueryParams = TypeOf; +export type UsageMetricsRequestSchemaQueryParams = TypeOf; export const UsageMetricsResponseSchema = { body: () => @@ -92,11 +72,40 @@ export const UsageMetricsResponseSchema = { schema.object({ name: schema.string(), data: schema.arrayOf( - schema.arrayOf(schema.number(), { minSize: 2, maxSize: 2 }) // Each data point is an array of 2 numbers + schema.object({ + x: schema.number(), + y: schema.number(), + }) ), }) ) ), }), }; -export type UsageMetricsResponseSchemaBody = TypeOf; +export type UsageMetricsResponseSchemaBody = Omit< + TypeOf, + 'metrics' +> & { + metrics: Partial>; +}; +export type MetricSeries = TypeOf< + typeof UsageMetricsResponseSchema.body +>['metrics'][MetricTypes][number]; + +export const UsageMetricsAutoOpsResponseSchema = { + body: () => + schema.object({ + metrics: schema.recordOf( + metricTypesSchema, + schema.arrayOf( + schema.object({ + name: schema.string(), + data: schema.arrayOf(schema.arrayOf(schema.number(), { minSize: 2, maxSize: 2 })), + }) + ) + ), + }), +}; +export type UsageMetricsAutoOpsResponseSchemaBody = TypeOf< + typeof UsageMetricsAutoOpsResponseSchema.body +>; diff --git a/x-pack/plugins/data_usage/public/app/components/chart_panel.tsx b/x-pack/plugins/data_usage/public/app/components/chart_panel.tsx index c7937ae149de9..1ba3f0fe3f454 100644 --- a/x-pack/plugins/data_usage/public/app/components/chart_panel.tsx +++ b/x-pack/plugins/data_usage/public/app/components/chart_panel.tsx @@ -19,8 +19,7 @@ import { } from '@elastic/charts'; import { i18n } from '@kbn/i18n'; import { LegendAction } from './legend_action'; -import { MetricTypes } from '../../../common/rest_types'; -import { MetricSeries } from '../types'; +import { MetricTypes, MetricSeries } from '../../../common/rest_types'; // TODO: Remove this when we have a title for each metric type type ChartKey = Extract; @@ -50,7 +49,7 @@ export const ChartPanel: React.FC = ({ }) => { const theme = useEuiTheme(); - const chartTimestamps = series.flatMap((stream) => stream.data.map((d) => d[0])); + const chartTimestamps = series.flatMap((stream) => stream.data.map((d) => d.x)); const [minTimestamp, maxTimestamp] = [Math.min(...chartTimestamps), Math.max(...chartTimestamps)]; @@ -72,6 +71,7 @@ export const ChartPanel: React.FC = ({ }, [idx, popoverOpen, togglePopover] ); + return ( @@ -94,9 +94,9 @@ export const ChartPanel: React.FC = ({ data={stream.data} xScaleType={ScaleType.Time} yScaleType={ScaleType.Linear} - xAccessor={0} // x is the first element in the tuple - yAccessors={[1]} // y is the second element in the tuple - stackAccessors={[0]} + xAccessor="x" + yAccessors={['y']} + stackAccessors={['x']} /> ))} @@ -118,6 +118,7 @@ export const ChartPanel: React.FC = ({ ); }; + const formatBytes = (bytes: number) => { return numeral(bytes).format('0.0 b'); }; diff --git a/x-pack/plugins/data_usage/public/app/components/charts.tsx b/x-pack/plugins/data_usage/public/app/components/charts.tsx index 6549f7e03830a..8d04324fb2246 100644 --- a/x-pack/plugins/data_usage/public/app/components/charts.tsx +++ b/x-pack/plugins/data_usage/public/app/components/charts.tsx @@ -6,11 +6,11 @@ */ import React, { useCallback, useState } from 'react'; import { EuiFlexGroup } from '@elastic/eui'; -import { MetricsResponse } from '../types'; import { MetricTypes } from '../../../common/rest_types'; import { ChartPanel } from './chart_panel'; +import { UsageMetricsResponseSchemaBody } from '../../../common/rest_types'; interface ChartsProps { - data: MetricsResponse; + data: UsageMetricsResponseSchemaBody; } export const Charts: React.FC = ({ data }) => { diff --git a/x-pack/plugins/data_usage/public/app/components/legend_action.tsx b/x-pack/plugins/data_usage/public/app/components/legend_action.tsx index a816d1f8eadda..c9059037c4445 100644 --- a/x-pack/plugins/data_usage/public/app/components/legend_action.tsx +++ b/x-pack/plugins/data_usage/public/app/components/legend_action.tsx @@ -14,6 +14,7 @@ import { EuiListGroupItem, EuiSpacer, } from '@elastic/eui'; +import { IndexManagementLocatorParams } from '@kbn/index-management-shared-types'; import { DatasetQualityLink } from './dataset_quality_link'; import { useKibanaContextForPlugin } from '../../utils/use_kibana'; @@ -39,12 +40,11 @@ export const LegendAction: React.FC = React.memo( const hasIndexManagementFeature = !!capabilities?.index_management; const onClickIndexManagement = useCallback(async () => { - // TODO: use proper index management locator https://github.com/elastic/kibana/issues/195083 - const dataQualityLocator = locators.get('MANAGEMENT_APP_LOCATOR'); - if (dataQualityLocator) { - await dataQualityLocator.navigate({ - sectionId: 'data', - appId: `index_management/data_streams/${label}`, + const locator = locators.get('INDEX_MANAGEMENT_LOCATOR_ID'); + if (locator) { + await locator.navigate({ + page: 'data_streams_details', + dataStreamName: label, }); } togglePopover(null); // Close the popover after action diff --git a/x-pack/plugins/data_usage/public/app/data_usage.tsx b/x-pack/plugins/data_usage/public/app/data_usage.tsx index c32f86d68b5bf..bea9f2b511a77 100644 --- a/x-pack/plugins/data_usage/public/app/data_usage.tsx +++ b/x-pack/plugins/data_usage/public/app/data_usage.tsx @@ -26,7 +26,6 @@ import { PLUGIN_NAME } from '../../common'; import { useGetDataUsageMetrics } from '../hooks/use_get_usage_metrics'; import { DEFAULT_DATE_RANGE_OPTIONS, useDateRangePicker } from './hooks/use_date_picker'; import { useDataUsageMetricsUrlParams } from './hooks/use_charts_url_params'; -import { MetricsResponse } from './types'; export const DataUsage = () => { const { @@ -42,37 +41,37 @@ export const DataUsage = () => { setUrlDateRangeFilter, } = useDataUsageMetricsUrlParams(); - const [queryParams, setQueryParams] = useState({ + const [metricsFilters, setMetricsFilters] = useState({ metricTypes: ['storage_retained', 'ingest_rate'], - dataStreams: [], + // TODO: Replace with data streams from /data_streams api + dataStreams: [ + '.alerts-ml.anomaly-detection-health.alerts-default', + '.alerts-stack.alerts-default', + ], from: DEFAULT_DATE_RANGE_OPTIONS.startDate, to: DEFAULT_DATE_RANGE_OPTIONS.endDate, }); useEffect(() => { if (!metricTypesFromUrl) { - setUrlMetricTypesFilter( - typeof queryParams.metricTypes !== 'string' - ? queryParams.metricTypes.join(',') - : queryParams.metricTypes - ); + setUrlMetricTypesFilter(metricsFilters.metricTypes.join(',')); } if (!startDateFromUrl || !endDateFromUrl) { - setUrlDateRangeFilter({ startDate: queryParams.from, endDate: queryParams.to }); + setUrlDateRangeFilter({ startDate: metricsFilters.from, endDate: metricsFilters.to }); } }, [ endDateFromUrl, metricTypesFromUrl, - queryParams.from, - queryParams.metricTypes, - queryParams.to, + metricsFilters.from, + metricsFilters.metricTypes, + metricsFilters.to, setUrlDateRangeFilter, setUrlMetricTypesFilter, startDateFromUrl, ]); useEffect(() => { - setQueryParams((prevState) => ({ + setMetricsFilters((prevState) => ({ ...prevState, metricTypes: metricTypesFromUrl?.length ? metricTypesFromUrl : prevState.metricTypes, dataStreams: dataStreamsFromUrl?.length ? dataStreamsFromUrl : prevState.dataStreams, @@ -89,7 +88,7 @@ export const DataUsage = () => { refetch: refetchDataUsageMetrics, } = useGetDataUsageMetrics( { - ...queryParams, + ...metricsFilters, from: dateRangePickerState.startDate, to: dateRangePickerState.endDate, }, @@ -140,7 +139,7 @@ export const DataUsage = () => { - {isFetched && data ? : } + {isFetched && data ? : } ); diff --git a/x-pack/plugins/data_usage/public/app/types.ts b/x-pack/plugins/data_usage/public/app/types.ts deleted file mode 100644 index 13f53bc2ea6dd..0000000000000 --- a/x-pack/plugins/data_usage/public/app/types.ts +++ /dev/null @@ -1,24 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { MetricTypes } from '../../common/rest_types'; - -export type DataPoint = [number, number]; // [timestamp, value] - -export interface MetricSeries { - name: string; // Name of the data stream - data: DataPoint[]; // Array of data points in tuple format [timestamp, value] -} -// Use MetricTypes dynamically as keys for the Metrics interface -export type Metrics = Partial>; - -export interface MetricsResponse { - metrics: Metrics; -} -export interface MetricsResponse { - metrics: Metrics; -} diff --git a/x-pack/plugins/data_usage/public/hooks/use_get_usage_metrics.ts b/x-pack/plugins/data_usage/public/hooks/use_get_usage_metrics.ts index 6b9860e997c12..3d648eb183f07 100644 --- a/x-pack/plugins/data_usage/public/hooks/use_get_usage_metrics.ts +++ b/x-pack/plugins/data_usage/public/hooks/use_get_usage_metrics.ts @@ -21,24 +21,24 @@ interface ErrorType { } export const useGetDataUsageMetrics = ( - query: UsageMetricsRequestSchemaQueryParams, + body: UsageMetricsRequestSchemaQueryParams, options: UseQueryOptions> = {} ): UseQueryResult> => { const http = useKibanaContextForPlugin().services.http; return useQuery>({ - queryKey: ['get-data-usage-metrics', query], + queryKey: ['get-data-usage-metrics', body], ...options, keepPreviousData: true, queryFn: async () => { - return http.get(DATA_USAGE_METRICS_API_ROUTE, { + return http.post(DATA_USAGE_METRICS_API_ROUTE, { version: '1', - query: { - from: query.from, - to: query.to, - metricTypes: query.metricTypes, - dataStreams: query.dataStreams, - }, + body: JSON.stringify({ + from: body.from, + to: body.to, + metricTypes: body.metricTypes, + dataStreams: body.dataStreams, + }), }); }, }); diff --git a/x-pack/plugins/data_usage/server/routes/internal/usage_metrics.ts b/x-pack/plugins/data_usage/server/routes/internal/usage_metrics.ts index 5bf3008ef668a..0013102f697fb 100644 --- a/x-pack/plugins/data_usage/server/routes/internal/usage_metrics.ts +++ b/x-pack/plugins/data_usage/server/routes/internal/usage_metrics.ts @@ -17,7 +17,7 @@ export const registerUsageMetricsRoute = ( ) => { if (dataUsageContext.serverConfig.enabled) { router.versioned - .get({ + .post({ access: 'internal', path: DATA_USAGE_METRICS_API_ROUTE, }) @@ -25,7 +25,9 @@ export const registerUsageMetricsRoute = ( { version: '1', validate: { - request: UsageMetricsRequestSchema, + request: { + body: UsageMetricsRequestSchema, + }, response: { 200: UsageMetricsResponseSchema, }, diff --git a/x-pack/plugins/data_usage/server/routes/internal/usage_metrics_handler.ts b/x-pack/plugins/data_usage/server/routes/internal/usage_metrics_handler.ts index 6f992c9fb2a38..09e9f88721c63 100644 --- a/x-pack/plugins/data_usage/server/routes/internal/usage_metrics_handler.ts +++ b/x-pack/plugins/data_usage/server/routes/internal/usage_metrics_handler.ts @@ -9,8 +9,10 @@ import { RequestHandler } from '@kbn/core/server'; import { IndicesGetDataStreamResponse } from '@elastic/elasticsearch/lib/api/types'; import { MetricTypes, + UsageMetricsAutoOpsResponseSchema, + UsageMetricsAutoOpsResponseSchemaBody, UsageMetricsRequestSchemaQueryParams, - UsageMetricsResponseSchema, + UsageMetricsResponseSchemaBody, } from '../../../common/rest_types'; import { DataUsageContext, DataUsageRequestHandlerContext } from '../../types'; @@ -34,45 +36,26 @@ export const getUsageMetricsHandler = ( const core = await context.core; const esClient = core.elasticsearch.client.asCurrentUser; - // @ts-ignore - const { from, to, metricTypes, dataStreams: dsNames, size } = request.query; + const { from, to, metricTypes, dataStreams: requestDsNames } = request.query; logger.debug(`Retrieving usage metrics`); const { data_streams: dataStreamsResponse }: IndicesGetDataStreamResponse = await esClient.indices.getDataStream({ - name: '*', + name: requestDsNames, expand_wildcards: 'all', }); - const hasDataStreams = dataStreamsResponse.length > 0; - let userDsNames: string[] = []; - - if (dsNames?.length) { - userDsNames = typeof dsNames === 'string' ? [dsNames] : dsNames; - } else if (!userDsNames.length && hasDataStreams) { - userDsNames = dataStreamsResponse.map((ds) => ds.name); - } - - // If no data streams are found, return an empty response - if (!userDsNames.length) { - return response.ok({ - body: { - metrics: {}, - }, - }); - } - const metrics = await fetchMetricsFromAutoOps({ from, to, metricTypes: formatStringParams(metricTypes) as MetricTypes[], - dataStreams: formatStringParams(userDsNames), + dataStreams: formatStringParams(dataStreamsResponse.map((ds) => ds.name)), }); + const processedMetrics = transformMetricsData(metrics); + return response.ok({ - body: { - metrics, - }, + body: processedMetrics, }); } catch (error) { logger.error(`Error retrieving usage metrics: ${error.message}`); @@ -94,7 +77,7 @@ const fetchMetricsFromAutoOps = async ({ }) => { // TODO: fetch data from autoOps using userDsNames /* - const response = await axios.post('https://api.auto-ops.{region}.{csp}.cloud.elastic.co/monitoring/serverless/v1/projects/{project_id}/metrics', { + const response = await axios.post({AUTOOPS_URL}, { from: Date.parse(from), to: Date.parse(to), metric_types: metricTypes, @@ -231,7 +214,25 @@ const fetchMetricsFromAutoOps = async ({ }, }; // Make sure data is what we expect - const validatedData = UsageMetricsResponseSchema.body().validate(mockData); + const validatedData = UsageMetricsAutoOpsResponseSchema.body().validate(mockData); - return validatedData.metrics; + return validatedData; }; +function transformMetricsData( + data: UsageMetricsAutoOpsResponseSchemaBody +): UsageMetricsResponseSchemaBody { + return { + metrics: Object.fromEntries( + Object.entries(data.metrics).map(([metricType, series]) => [ + metricType, + series.map((metricSeries) => ({ + name: metricSeries.name, + data: (metricSeries.data as Array<[number, number]>).map(([timestamp, value]) => ({ + x: timestamp, + y: value, + })), + })), + ]) + ), + }; +} diff --git a/x-pack/plugins/data_usage/tsconfig.json b/x-pack/plugins/data_usage/tsconfig.json index cecbeb654db30..d3754906475e9 100644 --- a/x-pack/plugins/data_usage/tsconfig.json +++ b/x-pack/plugins/data_usage/tsconfig.json @@ -28,6 +28,7 @@ "@kbn/core-http-browser", "@kbn/core-chrome-browser", "@kbn/features-plugin", + "@kbn/index-management-shared-types", ], "exclude": ["target/**/*"] } diff --git a/x-pack/plugins/elastic_assistant/server/__mocks__/docs_from_directory_loader.ts b/x-pack/plugins/elastic_assistant/server/__mocks__/docs_from_directory_loader.ts index 152d8c83987a3..e8cdf4b20dbde 100644 --- a/x-pack/plugins/elastic_assistant/server/__mocks__/docs_from_directory_loader.ts +++ b/x-pack/plugins/elastic_assistant/server/__mocks__/docs_from_directory_loader.ts @@ -8,43 +8,7 @@ import { Document } from 'langchain/document'; /** - * Mock LangChain `Document`s from `knowledge_base/esql/documentation`, loaded from a LangChain `DirectoryLoader` - */ -export const mockEsqlDocsFromDirectoryLoader: Document[] = [ - { - pageContent: - '[[esql-agg-avg]]\n=== `AVG`\nThe average of a numeric field.\n\n[source.merge.styled,esql]\n----\ninclude::{esql-specs}/stats.csv-spec[tag=avg]\n----\n[%header.monospaced.styled,format=dsv,separator=|]\n|===\ninclude::{esql-specs}/stats.csv-spec[tag=avg-result]\n|===\n\nThe result is always a `double` not matter the input type.\n', - metadata: { - source: - '/Users/andrew.goldstein/Projects/forks/andrew-goldstein/kibana/x-pack/plugins/elastic_assistant/server/knowledge_base/esql/documentation/aggregation_functions/avg.asciidoc', - }, - }, -]; - -/** - * Mock LangChain `Document`s from `knowledge_base/esql/language_definition`, loaded from a LangChain `DirectoryLoader` - */ -export const mockEsqlLanguageDocsFromDirectoryLoader: Document[] = [ - { - pageContent: - "lexer grammar EsqlBaseLexer;\n\nDISSECT : 'dissect' -> pushMode(EXPRESSION);\nDROP : 'drop' -> pushMode(SOURCE_IDENTIFIERS);\nENRICH : 'enrich' -> pushMode(SOURCE_IDENTIFIERS);\nEVAL : 'eval' -> pushMode(EXPRESSION);\nEXPLAIN : 'explain' -> pushMode(EXPLAIN_MODE);\nFROM : 'from' -> pushMode(SOURCE_IDENTIFIERS);\nGROK : 'grok' -> pushMode(EXPRESSION);\nINLINESTATS : 'inlinestats' -> pushMode(EXPRESSION);\nKEEP : 'keep' -> pushMode(SOURCE_IDENTIFIERS);\nLIMIT : 'limit' -> pushMode(EXPRESSION);\nMV_EXPAND : 'mv_expand' -> pushMode(SOURCE_IDENTIFIERS);\nPROJECT : 'project' -> pushMode(SOURCE_IDENTIFIERS);\nRENAME : 'rename' -> pushMode(SOURCE_IDENTIFIERS);\nROW : 'row' -> pushMode(EXPRESSION);\nSHOW : 'show' -> pushMode(EXPRESSION);\nSORT : 'sort' -> pushMode(EXPRESSION);\nSTATS : 'stats' -> pushMode(EXPRESSION);\nWHERE : 'where' -> pushMode(EXPRESSION);\nUNKNOWN_CMD : ~[ \\r\\n\\t[\\]/]+ -> pushMode(EXPRESSION);\n\nLINE_COMMENT\n : '//' ~[\\r\\n]* '\\r'? '\\n'? -> channel(HIDDEN)\n ;\n\nMULTILINE_COMMENT\n : '/*' (MULTILINE_COMMENT|.)*? '*/' -> channel(HIDDEN)\n ;\n\nWS\n : [ \\r\\n\\t]+ -> channel(HIDDEN)\n ;\n\n\nmode EXPLAIN_MODE;\nEXPLAIN_OPENING_BRACKET : '[' -> type(OPENING_BRACKET), pushMode(DEFAULT_MODE);\nEXPLAIN_PIPE : '|' -> type(PIPE), popMode;\nEXPLAIN_WS : WS -> channel(HIDDEN);\nEXPLAIN_LINE_COMMENT : LINE_COMMENT -> channel(HIDDEN);\nEXPLAIN_MULTILINE_COMMENT : MULTILINE_COMMENT -> channel(HIDDEN);\n\nmode EXPRESSION;\n\nPIPE : '|' -> popMode;\n\nfragment DIGIT\n : [0-9]\n ;\n\nfragment LETTER\n : [A-Za-z]\n ;\n\nfragment ESCAPE_SEQUENCE\n : '\\\\' [tnr\"\\\\]\n ;\n\nfragment UNESCAPED_CHARS\n : ~[\\r\\n\"\\\\]\n ;\n\nfragment EXPONENT\n : [Ee] [+-]? DIGIT+\n ;\n\nSTRING\n : '\"' (ESCAPE_SEQUENCE | UNESCAPED_CHARS)* '\"'\n | '\"\"\"' (~[\\r\\n])*? '\"\"\"' '\"'? '\"'?\n ;\n\nINTEGER_LITERAL\n : DIGIT+\n ;\n\nDECIMAL_LITERAL\n : DIGIT+ DOT DIGIT*\n | DOT DIGIT+\n | DIGIT+ (DOT DIGIT*)? EXPONENT\n | DOT DIGIT+ EXPONENT\n ;\n\nBY : 'by';\n\nAND : 'and';\nASC : 'asc';\nASSIGN : '=';\nCOMMA : ',';\nDESC : 'desc';\nDOT : '.';\nFALSE : 'false';\nFIRST : 'first';\nLAST : 'last';\nLP : '(';\nIN: 'in';\nIS: 'is';\nLIKE: 'like';\nNOT : 'not';\nNULL : 'null';\nNULLS : 'nulls';\nOR : 'or';\nPARAM: '?';\nRLIKE: 'rlike';\nRP : ')';\nTRUE : 'true';\nINFO : 'info';\nFUNCTIONS : 'functions';\n\nEQ : '==';\nNEQ : '!=';\nLT : '<';\nLTE : '<=';\nGT : '>';\nGTE : '>=';\n\nPLUS : '+';\nMINUS : '-';\nASTERISK : '*';\nSLASH : '/';\nPERCENT : '%';\n\n// Brackets are funny. We can happen upon a CLOSING_BRACKET in two ways - one\n// way is to start in an explain command which then shifts us to expression\n// mode. Thus, the two popModes on CLOSING_BRACKET. The other way could as\n// the start of a multivalued field constant. To line up with the double pop\n// the explain mode needs, we double push when we see that.\nOPENING_BRACKET : '[' -> pushMode(EXPRESSION), pushMode(EXPRESSION);\nCLOSING_BRACKET : ']' -> popMode, popMode;\n\n\nUNQUOTED_IDENTIFIER\n : LETTER (LETTER | DIGIT | '_')*\n // only allow @ at beginning of identifier to keep the option to allow @ as infix operator in the future\n // also, single `_` and `@` characters are not valid identifiers\n | ('_' | '@') (LETTER | DIGIT | '_')+\n ;\n\nQUOTED_IDENTIFIER\n : '`' ( ~'`' | '``' )* '`'\n ;\n\nEXPR_LINE_COMMENT\n : LINE_COMMENT -> channel(HIDDEN)\n ;\n\nEXPR_MULTILINE_COMMENT\n : MULTILINE_COMMENT -> channel(HIDDEN)\n ;\n\nEXPR_WS\n : WS -> channel(HIDDEN)\n ;\n\n\n\nmode SOURCE_IDENTIFIERS;\n\nSRC_PIPE : '|' -> type(PIPE), popMode;\nSRC_OPENING_BRACKET : '[' -> type(OPENING_BRACKET), pushMode(SOURCE_IDENTIFIERS), pushMode(SOURCE_IDENTIFIERS);\nSRC_CLOSING_BRACKET : ']' -> popMode, popMode, type(CLOSING_BRACKET);\nSRC_COMMA : ',' -> type(COMMA);\nSRC_ASSIGN : '=' -> type(ASSIGN);\nAS : 'as';\nMETADATA: 'metadata';\nON : 'on';\nWITH : 'with';\n\nSRC_UNQUOTED_IDENTIFIER\n : SRC_UNQUOTED_IDENTIFIER_PART+\n ;\n\nfragment SRC_UNQUOTED_IDENTIFIER_PART\n : ~[=`|,[\\]/ \\t\\r\\n]+\n | '/' ~[*/] // allow single / but not followed by another / or * which would start a comment\n ;\n\nSRC_QUOTED_IDENTIFIER\n : QUOTED_IDENTIFIER\n ;\n\nSRC_LINE_COMMENT\n : LINE_COMMENT -> channel(HIDDEN)\n ;\n\nSRC_MULTILINE_COMMENT\n : MULTILINE_COMMENT -> channel(HIDDEN)\n ;\n\nSRC_WS\n : WS -> channel(HIDDEN)\n ;\n", - metadata: { - source: - '/Users/andrew.goldstein/Projects/forks/andrew-goldstein/kibana/x-pack/plugins/elastic_assistant/server/knowledge_base/esql/language_definition/esql_base_lexer.g4', - }, - }, - { - pageContent: - "DISSECT=1\nDROP=2\nENRICH=3\nEVAL=4\nEXPLAIN=5\nFROM=6\nGROK=7\nINLINESTATS=8\nKEEP=9\nLIMIT=10\nMV_EXPAND=11\nPROJECT=12\nRENAME=13\nROW=14\nSHOW=15\nSORT=16\nSTATS=17\nWHERE=18\nUNKNOWN_CMD=19\nLINE_COMMENT=20\nMULTILINE_COMMENT=21\nWS=22\nEXPLAIN_WS=23\nEXPLAIN_LINE_COMMENT=24\nEXPLAIN_MULTILINE_COMMENT=25\nPIPE=26\nSTRING=27\nINTEGER_LITERAL=28\nDECIMAL_LITERAL=29\nBY=30\nAND=31\nASC=32\nASSIGN=33\nCOMMA=34\nDESC=35\nDOT=36\nFALSE=37\nFIRST=38\nLAST=39\nLP=40\nIN=41\nIS=42\nLIKE=43\nNOT=44\nNULL=45\nNULLS=46\nOR=47\nPARAM=48\nRLIKE=49\nRP=50\nTRUE=51\nINFO=52\nFUNCTIONS=53\nEQ=54\nNEQ=55\nLT=56\nLTE=57\nGT=58\nGTE=59\nPLUS=60\nMINUS=61\nASTERISK=62\nSLASH=63\nPERCENT=64\nOPENING_BRACKET=65\nCLOSING_BRACKET=66\nUNQUOTED_IDENTIFIER=67\nQUOTED_IDENTIFIER=68\nEXPR_LINE_COMMENT=69\nEXPR_MULTILINE_COMMENT=70\nEXPR_WS=71\nAS=72\nMETADATA=73\nON=74\nWITH=75\nSRC_UNQUOTED_IDENTIFIER=76\nSRC_QUOTED_IDENTIFIER=77\nSRC_LINE_COMMENT=78\nSRC_MULTILINE_COMMENT=79\nSRC_WS=80\nEXPLAIN_PIPE=81\n'dissect'=1\n'drop'=2\n'enrich'=3\n'eval'=4\n'explain'=5\n'from'=6\n'grok'=7\n'inlinestats'=8\n'keep'=9\n'limit'=10\n'mv_expand'=11\n'project'=12\n'rename'=13\n'row'=14\n'show'=15\n'sort'=16\n'stats'=17\n'where'=18\n'by'=30\n'and'=31\n'asc'=32\n'desc'=35\n'.'=36\n'false'=37\n'first'=38\n'last'=39\n'('=40\n'in'=41\n'is'=42\n'like'=43\n'not'=44\n'null'=45\n'nulls'=46\n'or'=47\n'?'=48\n'rlike'=49\n')'=50\n'true'=51\n'info'=52\n'functions'=53\n'=='=54\n'!='=55\n'<'=56\n'<='=57\n'>'=58\n'>='=59\n'+'=60\n'-'=61\n'*'=62\n'/'=63\n'%'=64\n']'=66\n'as'=72\n'metadata'=73\n'on'=74\n'with'=75\n", - metadata: { - source: - '/Users/andrew.goldstein/Projects/forks/andrew-goldstein/kibana/x-pack/plugins/elastic_assistant/server/knowledge_base/esql/language_definition/esql_base_lexer.tokens', - }, - }, -]; - -/** - * Mock LangChain `Document`s from `knowledge_base/esql/example_queries`, loaded from a LangChain `DirectoryLoader` + * Mock LangChain `Document`s loaded from a LangChain `DirectoryLoader` */ export const mockExampleQueryDocsFromDirectoryLoader: Document[] = [ { diff --git a/x-pack/plugins/elastic_assistant/server/ai_assistant_data_clients/knowledge_base/create_knowledge_base_entry.ts b/x-pack/plugins/elastic_assistant/server/ai_assistant_data_clients/knowledge_base/create_knowledge_base_entry.ts index 7dac58ddecc9b..aef66d406bf74 100644 --- a/x-pack/plugins/elastic_assistant/server/ai_assistant_data_clients/knowledge_base/create_knowledge_base_entry.ts +++ b/x-pack/plugins/elastic_assistant/server/ai_assistant_data_clients/knowledge_base/create_knowledge_base_entry.ts @@ -12,10 +12,11 @@ import { DocumentEntryCreateFields, KnowledgeBaseEntryCreateProps, KnowledgeBaseEntryResponse, + KnowledgeBaseEntryUpdateProps, Metadata, } from '@kbn/elastic-assistant-common'; import { getKnowledgeBaseEntry } from './get_knowledge_base_entry'; -import { CreateKnowledgeBaseEntrySchema } from './types'; +import { CreateKnowledgeBaseEntrySchema, UpdateKnowledgeBaseEntrySchema } from './types'; export interface CreateKnowledgeBaseEntryParams { esClient: ElasticsearchClient; @@ -77,6 +78,111 @@ export const createKnowledgeBaseEntry = async ({ } }; +interface TransformToUpdateSchemaProps { + user: AuthenticatedUser; + updatedAt: string; + entry: KnowledgeBaseEntryUpdateProps; + global?: boolean; +} + +export const transformToUpdateSchema = ({ + user, + updatedAt, + entry, + global = false, +}: TransformToUpdateSchemaProps): UpdateKnowledgeBaseEntrySchema => { + const base = { + id: entry.id, + updated_at: updatedAt, + updated_by: user.profile_uid ?? 'unknown', + name: entry.name, + type: entry.type, + users: global + ? [] + : [ + { + id: user.profile_uid, + name: user.username, + }, + ], + }; + + if (entry.type === 'index') { + const { inputSchema, outputFields, queryDescription, ...restEntry } = entry; + return { + ...base, + ...restEntry, + query_description: queryDescription, + input_schema: + entry.inputSchema?.map((schema) => ({ + field_name: schema.fieldName, + field_type: schema.fieldType, + description: schema.description, + })) ?? undefined, + output_fields: outputFields ?? undefined, + }; + } + return { + ...base, + kb_resource: entry.kbResource, + required: entry.required ?? false, + source: entry.source, + text: entry.text, + vector: undefined, + }; +}; + +export const getUpdateScript = ({ + entry, + isPatch, +}: { + entry: UpdateKnowledgeBaseEntrySchema; + isPatch?: boolean; +}) => { + return { + source: ` + if (params.assignEmpty == true || params.containsKey('name')) { + ctx._source.name = params.name; + } + if (params.assignEmpty == true || params.containsKey('type')) { + ctx._source.type = params.type; + } + if (params.assignEmpty == true || params.containsKey('users')) { + ctx._source.users = params.users; + } + if (params.assignEmpty == true || params.containsKey('query_description')) { + ctx._source.query_description = params.query_description; + } + if (params.assignEmpty == true || params.containsKey('input_schema')) { + ctx._source.input_schema = params.input_schema; + } + if (params.assignEmpty == true || params.containsKey('output_fields')) { + ctx._source.output_fields = params.output_fields; + } + if (params.assignEmpty == true || params.containsKey('kb_resource')) { + ctx._source.kb_resource = params.kb_resource; + } + if (params.assignEmpty == true || params.containsKey('required')) { + ctx._source.required = params.required; + } + if (params.assignEmpty == true || params.containsKey('source')) { + ctx._source.source = params.source; + } + if (params.assignEmpty == true || params.containsKey('text')) { + ctx._source.text = params.text; + } + ctx._source.updated_at = params.updated_at; + ctx._source.updated_by = params.updated_by; + `, + lang: 'painless', + params: { + ...entry, // when assigning undefined in painless, it will remove property and wil set it to null + // for patch we don't want to remove unspecified value in payload + assignEmpty: !(isPatch ?? true), + }, + }; +}; + interface TransformToCreateSchemaProps { createdAt: string; spaceId: string; diff --git a/x-pack/plugins/elastic_assistant/server/ai_assistant_data_clients/knowledge_base/helpers.ts b/x-pack/plugins/elastic_assistant/server/ai_assistant_data_clients/knowledge_base/helpers.ts index 8ff8de6cfb408..de76a38135f0b 100644 --- a/x-pack/plugins/elastic_assistant/server/ai_assistant_data_clients/knowledge_base/helpers.ts +++ b/x-pack/plugins/elastic_assistant/server/ai_assistant_data_clients/knowledge_base/helpers.ts @@ -6,6 +6,7 @@ */ import { z } from '@kbn/zod'; +import { get } from 'lodash'; import { DynamicStructuredTool } from '@langchain/core/tools'; import { errors } from '@elastic/elasticsearch'; import { QueryDslQueryContainer, SearchRequest } from '@elastic/elasticsearch/lib/api/types'; @@ -189,7 +190,7 @@ export const getStructuredToolForIndexEntry = ({ standard: { query: { nested: { - path: 'semantic_text.inference.chunks', + path: `${indexEntry.field}.inference.chunks`, query: { sparse_vector: { inference_id: elserId, @@ -220,7 +221,7 @@ export const getStructuredToolForIndexEntry = ({ }, {}); } return { - text: (hit._source as { text: string }).text, + text: get(hit._source, `${indexEntry.field}.inference.chunks[0].text`), }; }); diff --git a/x-pack/plugins/elastic_assistant/server/ai_assistant_data_clients/knowledge_base/index.ts b/x-pack/plugins/elastic_assistant/server/ai_assistant_data_clients/knowledge_base/index.ts index 7f665fa7f9a16..1906f59ab4b32 100644 --- a/x-pack/plugins/elastic_assistant/server/ai_assistant_data_clients/knowledge_base/index.ts +++ b/x-pack/plugins/elastic_assistant/server/ai_assistant_data_clients/knowledge_base/index.ts @@ -15,6 +15,7 @@ import { Document } from 'langchain/document'; import type { SavedObjectsClientContract } from '@kbn/core-saved-objects-api-server'; import { DocumentEntryType, + DocumentEntry, IndexEntry, KnowledgeBaseEntryCreateProps, KnowledgeBaseEntryResponse, @@ -25,7 +26,6 @@ import { QueryDslQueryContainer } from '@elastic/elasticsearch/lib/api/typesWith import { StructuredTool } from '@langchain/core/tools'; import { ElasticsearchClient } from '@kbn/core/server'; import { AIAssistantDataClient, AIAssistantDataClientParams } from '..'; -import { loadESQL } from '../../lib/langchain/content_loaders/esql_loader'; import { AssistantToolParams, GetElser } from '../../types'; import { createKnowledgeBaseEntry, @@ -200,17 +200,14 @@ export class AIAssistantKnowledgeBaseDataClient extends AIAssistantDataClient { * * @param options * @param options.soClient SavedObjectsClientContract for installing ELSER so that ML SO's are in sync - * @param options.installEsqlDocs Whether to install ESQL documents as part of setup (e.g. not needed in test env) * * @returns Promise */ public setupKnowledgeBase = async ({ soClient, - installEsqlDocs = true, installSecurityLabsDocs = true, }: { soClient: SavedObjectsClientContract; - installEsqlDocs?: boolean; installSecurityLabsDocs?: boolean; }): Promise => { if (this.options.getIsKBSetupInProgress()) { @@ -254,15 +251,6 @@ export class AIAssistantKnowledgeBaseDataClient extends AIAssistantDataClient { } this.options.logger.debug(`Checking if Knowledge Base docs have been loaded...`); - if (installEsqlDocs) { - const kbDocsLoaded = await this.isESQLDocsLoaded(); - if (!kbDocsLoaded) { - this.options.logger.debug(`Loading KB docs...`); - await loadESQL(this, this.options.logger); - } else { - this.options.logger.debug(`Knowledge Base docs already loaded!`); - } - } if (installSecurityLabsDocs) { const labsDocsLoaded = await this.isSecurityLabsDocsLoaded(); @@ -444,7 +432,9 @@ export class AIAssistantKnowledgeBaseDataClient extends AIAssistantDataClient { ); this.options.logger.debug( () => - `getKnowledgeBaseDocuments() - Similarity Search Results:\n ${JSON.stringify(results)}` + `getKnowledgeBaseDocuments() - Similarity Search returned [${JSON.stringify( + results.length + )}] results` ); return results; @@ -454,6 +444,47 @@ export class AIAssistantKnowledgeBaseDataClient extends AIAssistantDataClient { } }; + /** + * Returns all global and current user's private `required` document entries. + */ + public getRequiredKnowledgeBaseDocumentEntries = async (): Promise => { + const user = this.options.currentUser; + if (user == null) { + throw new Error( + 'Authenticated user not found! Ensure kbDataClient was initialized from a request.' + ); + } + + try { + const userFilter = getKBUserFilter(user); + const results = await this.findDocuments({ + // Note: This is a magic number to set some upward bound as to not blow the context with too + // many historical KB entries. Ideally we'd query for all and token trim. + perPage: 100, + page: 1, + sortField: 'created_at', + sortOrder: 'asc', + filter: `${userFilter} AND type:document AND kb_resource:user AND required:true`, + }); + this.options.logger.debug( + `kbDataClient.getRequiredKnowledgeBaseDocumentEntries() - results:\n${JSON.stringify( + results + )}` + ); + + if (results) { + return transformESSearchToKnowledgeBaseEntry(results.data) as DocumentEntry[]; + } + } catch (e) { + this.options.logger.error( + `kbDataClient.getRequiredKnowledgeBaseDocumentEntries() - Failed to fetch DocumentEntries` + ); + return []; + } + + return []; + }; + /** * Creates a new Knowledge Base Entry. * @@ -492,7 +523,10 @@ export class AIAssistantKnowledgeBaseDataClient extends AIAssistantDataClient { }; /** - * Returns AssistantTools for any 'relevant' KB IndexEntries that exist in the knowledge base + * Returns AssistantTools for any 'relevant' KB IndexEntries that exist in the knowledge base. + * + * Note: Accepts esClient so retrieval can be scoped to the current user as esClient on kbDataClient + * is scoped to system user. */ public getAssistantTools = async ({ assistantToolParams, @@ -520,7 +554,7 @@ export class AIAssistantKnowledgeBaseDataClient extends AIAssistantDataClient { page: 1, sortField: 'created_at', sortOrder: 'asc', - filter: `${userFilter}${` AND type:index`}`, // TODO: Support global tools (no user filter), and filter by space as well + filter: `${userFilter} AND type:index`, }); this.options.logger.debug( `kbDataClient.getAssistantTools() - results:\n${JSON.stringify(results, null, 2)}` diff --git a/x-pack/plugins/elastic_assistant/server/ai_assistant_data_clients/knowledge_base/types.ts b/x-pack/plugins/elastic_assistant/server/ai_assistant_data_clients/knowledge_base/types.ts index ecf9260e999d2..3de1a15d79b2a 100644 --- a/x-pack/plugins/elastic_assistant/server/ai_assistant_data_clients/knowledge_base/types.ts +++ b/x-pack/plugins/elastic_assistant/server/ai_assistant_data_clients/knowledge_base/types.ts @@ -82,6 +82,39 @@ export interface LegacyEsKnowledgeBaseEntrySchema { model_id: string; }; } +export interface UpdateKnowledgeBaseEntrySchema { + id: string; + created_at?: string; + created_by?: string; + updated_at?: string; + updated_by?: string; + users?: Array<{ + id?: string; + name?: string; + }>; + name?: string; + type?: string; + // Document Entry Fields + kb_resource?: string; + required?: boolean; + source?: string; + text?: string; + vector?: { + tokens: Record; + model_id: string; + }; + // Index Entry Fields + index?: string; + field?: string; + description?: string; + query_description?: string; + input_schema?: Array<{ + field_name: string; + field_type: string; + description: string; + }>; + output_fields?: string[]; +} export interface CreateKnowledgeBaseEntrySchema { '@timestamp'?: string; diff --git a/x-pack/plugins/elastic_assistant/server/ai_assistant_service/index.ts b/x-pack/plugins/elastic_assistant/server/ai_assistant_service/index.ts index 942f94c203873..08912f41a8bbc 100644 --- a/x-pack/plugins/elastic_assistant/server/ai_assistant_service/index.ts +++ b/x-pack/plugins/elastic_assistant/server/ai_assistant_service/index.ts @@ -84,6 +84,7 @@ export class AIAssistantService { private isKBSetupInProgress: boolean = false; // Temporary 'feature flag' to determine if we should initialize the new kb mappings, toggled when accessing kbDataClient private v2KnowledgeBaseEnabled: boolean = false; + private hasInitializedV2KnowledgeBase: boolean = false; constructor(private readonly options: AIAssistantServiceOpts) { this.initialized = false; @@ -363,8 +364,13 @@ export class AIAssistantService { // If either v2 KB or a modelIdOverride is provided, we need to reinitialize all persistence resources to make sure // they're using the correct model/mappings. Technically all existing KB data is stale since it was created // with a different model/mappings, but modelIdOverride is only intended for testing purposes at this time - if (opts.v2KnowledgeBaseEnabled || opts.modelIdOverride != null) { + // Added hasInitializedV2KnowledgeBase to prevent the console noise from re-init on each KB request + if ( + !this.hasInitializedV2KnowledgeBase && + (opts.v2KnowledgeBaseEnabled || opts.modelIdOverride != null) + ) { await this.initializeResources(); + this.hasInitializedV2KnowledgeBase = true; } const res = await this.checkResourcesInstallation(opts); diff --git a/x-pack/plugins/elastic_assistant/server/knowledge_base/esql/documentation/esql_commands.asciidoc b/x-pack/plugins/elastic_assistant/server/knowledge_base/esql/documentation/esql_commands.asciidoc deleted file mode 100644 index 8b0e99344add1..0000000000000 --- a/x-pack/plugins/elastic_assistant/server/knowledge_base/esql/documentation/esql_commands.asciidoc +++ /dev/null @@ -1,63 +0,0 @@ -[[esql-commands]] -=== {esql} commands - -++++ -Commands -++++ - -// tag::source_commands[] -==== Source commands - -An {esql} source command produces a table, typically with data from {es}. An {esql} query must start with a source command. - -image::images/esql/source-command.svg[A source command producing a table from {es},align="center"] - -{esql} supports these source commands: - -* <> -* <> -* <> - -// end::source_command[] - -// tag::proc_commands[] -==== Processing commands - -{esql} processing commands change an input table by adding, removing, or changing -rows and columns. - -image::images/esql/processing-command.svg[A processing command changing an input table,align="center"] - -{esql} supports these processing commands: - -* <> -* <> -* <> -* <> -* <> -* <> -* <> -* <> -* <> -* <> -* <> -* <> - -// end::proc_command[] - -include::source-commands/from.asciidoc[] -include::source-commands/row.asciidoc[] -include::source-commands/show.asciidoc[] - -include::processing-commands/dissect.asciidoc[] -include::processing-commands/drop.asciidoc[] -include::processing-commands/enrich.asciidoc[] -include::processing-commands/eval.asciidoc[] -include::processing-commands/grok.asciidoc[] -include::processing-commands/keep.asciidoc[] -include::processing-commands/limit.asciidoc[] -include::processing-commands/mv_expand.asciidoc[] -include::processing-commands/rename.asciidoc[] -include::processing-commands/sort.asciidoc[] -include::processing-commands/stats.asciidoc[] -include::processing-commands/where.asciidoc[] diff --git a/x-pack/plugins/elastic_assistant/server/knowledge_base/esql/documentation/esql_enrich_data.asciidoc b/x-pack/plugins/elastic_assistant/server/knowledge_base/esql/documentation/esql_enrich_data.asciidoc deleted file mode 100644 index 9708728e6b305..0000000000000 --- a/x-pack/plugins/elastic_assistant/server/knowledge_base/esql/documentation/esql_enrich_data.asciidoc +++ /dev/null @@ -1,126 +0,0 @@ -[[esql-enrich-data]] -=== Enrich data - -++++ -Enrich data -++++ - -You can use {esql}'s <> processing command to enrich a table with -data from indices in {es}. - -For example, you can use `ENRICH` to: - -* Identify web services or vendors based on known IP addresses -* Add product information to retail orders based on product IDs -* Supplement contact information based on an email address - -[[esql-how-enrich-works]] -==== How the `ENRICH` command works - -The `ENRICH` command adds new columns to a table, with data from {es} indices. -It requires a few special components: - -image::images/esql/esql-enrich.png[align="center"] - -[[esql-enrich-policy]] -Enrich policy:: -+ --- -A set of configuration options used to add the right enrich data to the input -table. - -An enrich policy contains: - -include::../ingest/enrich.asciidoc[tag=enrich-policy-fields] - -After <>, it must be -<> before it can be used. Executing an -enrich policy uses data from the policy's source indices to create a streamlined -system index called the _enrich index_. The `ENRICH` command uses this index to -match and enrich an input table. --- - -[[esql-source-index]] -Source index:: -An index which stores enrich data that the `ENRICH` command can add to input -tables. You can create and manage these indices just like a regular {es} index. -You can use multiple source indices in an enrich policy. You also can use the -same source index in multiple enrich policies. - -[[esql-enrich-index]] -Enrich index:: -+ --- -A special system index tied to a specific enrich policy. - -Directly matching rows from input tables to documents in source indices could be -slow and resource intensive. To speed things up, the `ENRICH` command uses an -enrich index. - -include::../ingest/enrich.asciidoc[tag=enrich-index] --- - -[[esql-set-up-enrich-policy]] -==== Set up an enrich policy - -To start using `ENRICH`, follow these steps: - -. Check the <>. -. <>. -. <>. -. <>. -. <> - -Once you have enrich policies set up, you can <> and <>. - -[IMPORTANT] -==== -The `ENRICH` command performs several operations and may impact the speed of -your query. -==== - -[[esql-enrich-prereqs]] -==== Prerequisites - -include::{es-repo-dir}/ingest/apis/enrich/put-enrich-policy.asciidoc[tag=enrich-policy-api-prereqs] - -[[esql-create-enrich-source-index]] -==== Add enrich data - -include::../ingest/enrich.asciidoc[tag=create-enrich-source-index] - -[[esql-create-enrich-policy]] -==== Create an enrich policy - -include::../ingest/enrich.asciidoc[tag=create-enrich-policy] - -[[esql-execute-enrich-policy]] -==== Execute the enrich policy - -include::../ingest/enrich.asciidoc[tag=execute-enrich-policy1] - -image::images/esql/esql-enrich-policy.png[align="center"] - -include::../ingest/enrich.asciidoc[tag=execute-enrich-policy2] - -[[esql-use-enrich]] -==== Use the enrich policy - -After the policy has been executed, you can use the <> to enrich your data. - -image::images/esql/esql-enrich-command.png[align="center",width=50%] - -include::processing-commands/enrich.asciidoc[tag=examples] - -[[esql-update-enrich-data]] -==== Update an enrich index - -include::{es-repo-dir}/ingest/apis/enrich/execute-enrich-policy.asciidoc[tag=update-enrich-index] - -[[esql-update-enrich-policies]] -==== Update an enrich policy - -include::../ingest/enrich.asciidoc[tag=update-enrich-policy] diff --git a/x-pack/plugins/elastic_assistant/server/knowledge_base/esql/documentation/esql_functions.asciidoc b/x-pack/plugins/elastic_assistant/server/knowledge_base/esql/documentation/esql_functions.asciidoc deleted file mode 100644 index b921719fc097b..0000000000000 --- a/x-pack/plugins/elastic_assistant/server/knowledge_base/esql/documentation/esql_functions.asciidoc +++ /dev/null @@ -1,140 +0,0 @@ -[[esql-functions]] -== {esql} functions - -++++ -Functions -++++ - -<>, <> and <> support -these functions: - -* <> -* <> -* <> -* <> -* <> -* <> -* <> -* <> -* <> -* <> -* <> -* <> -* <> -* <> -* <> -* <> -* <> -* <> -* <> -* <> -* <> -* <> -* <> -* <> -* <> -* <> -* <> -* <> -* <> -* <> -* <> -* <> -* <> -* <> -* <> -* <> -* <> -* <> -* <> -* <> -* <> -* <> -* <> -* <> -* <> -* <> -* <> -* <> -* <> -* <> -* <> -* <> -* <> -* <> -* <> -* <> -* <> -* <> -* <> -* <> -* <> -* <> -* <> -* <> - -include::functions/abs.asciidoc[] -include::functions/acos.asciidoc[] -include::functions/asin.asciidoc[] -include::functions/atan.asciidoc[] -include::functions/atan2.asciidoc[] -include::functions/auto_bucket.asciidoc[] -include::functions/case.asciidoc[] -include::functions/ceil.asciidoc[] -include::functions/cidr_match.asciidoc[] -include::functions/coalesce.asciidoc[] -include::functions/concat.asciidoc[] -include::functions/cos.asciidoc[] -include::functions/cosh.asciidoc[] -include::functions/date_extract.asciidoc[] -include::functions/date_format.asciidoc[] -include::functions/date_parse.asciidoc[] -include::functions/date_trunc.asciidoc[] -include::functions/e.asciidoc[] -include::functions/ends_with.asciidoc[] -include::functions/floor.asciidoc[] -include::functions/greatest.asciidoc[] -include::functions/is_finite.asciidoc[] -include::functions/is_infinite.asciidoc[] -include::functions/is_nan.asciidoc[] -include::functions/least.asciidoc[] -include::functions/left.asciidoc[] -include::functions/length.asciidoc[] -include::functions/log10.asciidoc[] -include::functions/ltrim.asciidoc[] -include::functions/mv_avg.asciidoc[] -include::functions/mv_concat.asciidoc[] -include::functions/mv_count.asciidoc[] -include::functions/mv_dedupe.asciidoc[] -include::functions/mv_max.asciidoc[] -include::functions/mv_median.asciidoc[] -include::functions/mv_min.asciidoc[] -include::functions/mv_sum.asciidoc[] -include::functions/now.asciidoc[] -include::functions/pi.asciidoc[] -include::functions/pow.asciidoc[] -include::functions/replace.asciidoc[] -include::functions/right.asciidoc[] -include::functions/round.asciidoc[] -include::functions/rtrim.asciidoc[] -include::functions/sin.asciidoc[] -include::functions/sinh.asciidoc[] -include::functions/split.asciidoc[] -include::functions/sqrt.asciidoc[] -include::functions/starts_with.asciidoc[] -include::functions/substring.asciidoc[] -include::functions/tan.asciidoc[] -include::functions/tanh.asciidoc[] -include::functions/tau.asciidoc[] -include::functions/to_boolean.asciidoc[] -include::functions/to_datetime.asciidoc[] -include::functions/to_degrees.asciidoc[] -include::functions/to_double.asciidoc[] -include::functions/to_integer.asciidoc[] -include::functions/to_ip.asciidoc[] -include::functions/to_long.asciidoc[] -include::functions/to_radians.asciidoc[] -include::functions/to_string.asciidoc[] -include::functions/to_unsigned_long.asciidoc[] -include::functions/to_version.asciidoc[] -include::functions/trim.asciidoc[] diff --git a/x-pack/plugins/elastic_assistant/server/knowledge_base/esql/documentation/esql_functions_operators.asciidoc b/x-pack/plugins/elastic_assistant/server/knowledge_base/esql/documentation/esql_functions_operators.asciidoc deleted file mode 100644 index 375bb4ee9dd00..0000000000000 --- a/x-pack/plugins/elastic_assistant/server/knowledge_base/esql/documentation/esql_functions_operators.asciidoc +++ /dev/null @@ -1,43 +0,0 @@ -[[esql-functions-operators]] -=== {esql} functions and operators - -++++ -Functions and operators -++++ - -{esql} provides a comprehensive set of functions and operators for working with data. -The functions are divided into the following categories: - -[[esql-functions]] -<>:: -include::functions/aggregation-functions.asciidoc[tag=agg_list] - -<>:: -include::functions/math-functions.asciidoc[tag=math_list] - -<>:: -include::functions/string-functions.asciidoc[tag=string_list] - -<>:: -include::functions/date-time-functions.asciidoc[tag=date_list] - -<>:: -include::functions/type-conversion-functions.asciidoc[tag=type_list] - -<>:: -include::functions/conditional-functions-and-expressions.asciidoc[tag=cond_list] - -<>:: -include::functions/mv-functions.asciidoc[tag=mv_list] - -<>:: -include::functions/operators.asciidoc[tag=op_list] - -include::functions/aggregation-functions.asciidoc[] -include::functions/math-functions.asciidoc[] -include::functions/string-functions.asciidoc[] -include::functions/date-time-functions.asciidoc[] -include::functions/type-conversion-functions.asciidoc[] -include::functions/conditional-functions-and-expressions.asciidoc[] -include::functions/mv-functions.asciidoc[] -include::functions/operators.asciidoc[] diff --git a/x-pack/plugins/elastic_assistant/server/knowledge_base/esql/documentation/esql_get_started.asciidoc b/x-pack/plugins/elastic_assistant/server/knowledge_base/esql/documentation/esql_get_started.asciidoc deleted file mode 100644 index 1f3cdf85c173e..0000000000000 --- a/x-pack/plugins/elastic_assistant/server/knowledge_base/esql/documentation/esql_get_started.asciidoc +++ /dev/null @@ -1,8 +0,0 @@ -[[esql-getting-started]] -== Getting started with {esql} - -++++ -Getting started -++++ - -coming::[8.11] \ No newline at end of file diff --git a/x-pack/plugins/elastic_assistant/server/knowledge_base/esql/documentation/esql_kibana.asciidoc b/x-pack/plugins/elastic_assistant/server/knowledge_base/esql/documentation/esql_kibana.asciidoc deleted file mode 100644 index 534cba22ed1a1..0000000000000 --- a/x-pack/plugins/elastic_assistant/server/knowledge_base/esql/documentation/esql_kibana.asciidoc +++ /dev/null @@ -1,15 +0,0 @@ -[[esql-kibana]] -== Using {esql} in {kib} - -++++ -Kibana -++++ - - -Use {esql} in Discover to explore a data set. From the data view dropdown, -select *Try {esql}* to get started. - -NOTE: {esql} queries in Discover and Lens are subject to the time range selected -with the time filter. - - diff --git a/x-pack/plugins/elastic_assistant/server/knowledge_base/esql/documentation/esql_language.asciidoc b/x-pack/plugins/elastic_assistant/server/knowledge_base/esql/documentation/esql_language.asciidoc deleted file mode 100644 index 2becd04cec948..0000000000000 --- a/x-pack/plugins/elastic_assistant/server/knowledge_base/esql/documentation/esql_language.asciidoc +++ /dev/null @@ -1,23 +0,0 @@ -[[esql-language]] -== Working with the {esql} language - -++++ -Working with the {esql} language -++++ - -Detailed information about the {esql} language: - -* <> -* <> -* <> -* <> -* <> -* <> - -include::esql-syntax.asciidoc[] -include::esql-commands.asciidoc[] -include::esql-functions-operators.asciidoc[] -include::multivalued-fields.asciidoc[] -include::metadata-fields.asciidoc[] -include::esql-enrich-data.asciidoc[] - diff --git a/x-pack/plugins/elastic_assistant/server/knowledge_base/esql/documentation/esql_limitations.asciidoc b/x-pack/plugins/elastic_assistant/server/knowledge_base/esql/documentation/esql_limitations.asciidoc deleted file mode 100644 index f39ff73744276..0000000000000 --- a/x-pack/plugins/elastic_assistant/server/knowledge_base/esql/documentation/esql_limitations.asciidoc +++ /dev/null @@ -1,32 +0,0 @@ -[[esql-limitations]] -== {esql} limitations - -++++ -Limitations -++++ - -[discrete] -[[esql-supported-types]] -=== Supported types - -* {esql} currently supports the following <>: - -** `alias` -** `boolean` -** `date` -** `double` (`float`, `half_float`, `scaled_float` are represented as `double`) -** `ip` -** `keyword` family including `keyword`, `constant_keyword`, and `wildcard` -** `int` (`short` and `byte` are represented as `int`) -** `long` -** `null` -** `text` -** `unsigned_long` -** `version` - -[discrete] -[[esql-max-rows]] -=== 10,000 row maximum - -A single query will not return more than 10,000 rows, regardless of the -`LIMIT` command's value. \ No newline at end of file diff --git a/x-pack/plugins/elastic_assistant/server/knowledge_base/esql/documentation/esql_query_api.asciidoc b/x-pack/plugins/elastic_assistant/server/knowledge_base/esql/documentation/esql_query_api.asciidoc deleted file mode 100644 index 437871d31a88f..0000000000000 --- a/x-pack/plugins/elastic_assistant/server/knowledge_base/esql/documentation/esql_query_api.asciidoc +++ /dev/null @@ -1,97 +0,0 @@ -[[esql-query-api]] -== {esql} query API -++++ -{esql} query API -++++ - -Returns search results for an <> query. - -[source,console] ----- -POST /_query -{ - "query": """ - FROM library - | EVAL year = DATE_TRUNC(1 YEARS, release_date) - | STATS MAX(page_count) BY year - | SORT year - | LIMIT 5 - """ -} ----- -// TEST[setup:library] - -[discrete] -[[esql-query-api-request]] -=== {api-request-title} - -`POST _query` - -[discrete] -[[esql-query-api-prereqs]] -=== {api-prereq-title} - -* If the {es} {security-features} are enabled, you must have the `read` -<> for the data stream, index, -or alias you search. - -[discrete] -[[esql-query-api-query-params]] -=== {api-query-parms-title} - -`delimiter`:: -(Optional, string) Separator for CSV results. Defaults to `,`. The API only -supports this parameter for CSV responses. - -`format`:: -(Optional, string) Format for the response. For valid values, refer to -<>. -+ -You can also specify a format using the `Accept` HTTP header. If you specify -both this parameter and the `Accept` HTTP header, this parameter takes -precedence. - -[discrete] -[role="child_attributes"] -[[esql-query-api-request-body]] -=== {api-request-body-title} - -`columnar`:: -(Optional, Boolean) If `true`, returns results in a columnar format. Defaults to -`false`. The API only supports this parameter for CBOR, JSON, SMILE, and YAML -responses. See <>. - -`params`:: -(Optional, array) Values for parameters in the `query`. For syntax, refer to -<>. - -`query`:: -(Required, object) {esql} query to run. For syntax, refer to <>. - -[[esql-search-api-time-zone]] -`time_zone`:: -(Optional, string) ISO-8601 time zone ID for the search. Several {esql} -date/time functions use this time zone. Defaults to `Z` (UTC). - -[discrete] -[role="child_attributes"] -[[esql-query-api-response-body]] -=== {api-response-body-title} - -`columns`:: -(array of objects) -Column headings for the search results. Each object is a column. -+ -.Properties of `columns` objects -[%collapsible%open] -==== -`name`:: -(string) Name of the column. - -`type`:: -(string) Data type for the column. -==== - -`rows`:: -(array of arrays) -Values for the search results. diff --git a/x-pack/plugins/elastic_assistant/server/knowledge_base/esql/documentation/esql_rest.asciidoc b/x-pack/plugins/elastic_assistant/server/knowledge_base/esql/documentation/esql_rest.asciidoc deleted file mode 100644 index 55c9946ad08b4..0000000000000 --- a/x-pack/plugins/elastic_assistant/server/knowledge_base/esql/documentation/esql_rest.asciidoc +++ /dev/null @@ -1,249 +0,0 @@ -[[esql-rest]] -== {esql} REST API - -++++ -REST API -++++ - -[discrete] -[[esql-rest-overview]] -=== Overview - -The <> accepts an {esql} query string in the -`query` parameter, runs it, and returns the results. For example: - -[source,console] ----- -POST /_query?format=txt -{ - "query": "FROM library | KEEP author, name, page_count, release_date | SORT page_count DESC | LIMIT 5" -} ----- -// TEST[setup:library] - -Which returns: - -[source,text] ----- - author | name | page_count | release_date ------------------+--------------------+---------------+------------------------ -Peter F. Hamilton|Pandora's Star |768 |2004-03-02T00:00:00.000Z -Vernor Vinge |A Fire Upon the Deep|613 |1992-06-01T00:00:00.000Z -Frank Herbert |Dune |604 |1965-06-01T00:00:00.000Z -Alastair Reynolds|Revelation Space |585 |2000-03-15T00:00:00.000Z -James S.A. Corey |Leviathan Wakes |561 |2011-06-02T00:00:00.000Z ----- -// TESTRESPONSE[s/\|/\\|/ s/\+/\\+/] -// TESTRESPONSE[non_json] - -[discrete] -[[esql-kibana-console]] -=== Kibana Console - -If you are using {kibana-ref}/console-kibana.html[Kibana Console] (which is -highly recommended), take advantage of the triple quotes `"""` when creating the -query. This not only automatically escapes double quotes (`"`) inside the query -string but also supports multi-line requests: - -// tag::esql-query-api[] -[source,console] ----- -POST /_query?format=txt -{ - "query": """ - FROM library - | KEEP author, name, page_count, release_date - | SORT page_count DESC - | LIMIT 5 - """ -} ----- -// TEST[setup:library] - -[discrete] -[[esql-rest-format]] -=== Response formats - -{esql} can return the data in the following human readable and binary formats. -You can set the format by specifying the `format` parameter in the URL or by -setting the `Accept` or `Content-Type` HTTP header. - -NOTE: The URL parameter takes precedence over the HTTP headers. If neither is -specified then the response is returned in the same format as the request. - -[cols="m,4m,8"] - -|=== -s|`format` -s|HTTP header -s|Description - -3+h| Human readable - -|csv -|text/csv -|{wikipedia}/Comma-separated_values[Comma-separated values] - -|json -|application/json -|https://www.json.org/[JSON] (JavaScript Object Notation) human-readable format - -|tsv -|text/tab-separated-values -|{wikipedia}/Tab-separated_values[Tab-separated values] - -|txt -|text/plain -|CLI-like representation - -|yaml -|application/yaml -|{wikipedia}/YAML[YAML] (YAML Ain't Markup Language) human-readable format - -3+h| Binary - -|cbor -|application/cbor -|https://cbor.io/[Concise Binary Object Representation] - -|smile -|application/smile -|{wikipedia}/Smile_(data_interchange_format)[Smile] binary data format similar -to CBOR - -|=== - -The `csv` format accepts a formatting URL query attribute, `delimiter`, which -indicates which character should be used to separate the CSV values. It defaults -to comma (`,`) and cannot take any of the following values: double quote (`"`), -carriage-return (`\r`) and new-line (`\n`). The tab (`\t`) can also not be used. -Use the `tsv` format instead. - -[discrete] -[[esql-rest-filtering]] -=== Filtering using {es} Query DSL - -Specify a Query DSL query in the `filter` parameter to filter the set of -documents that an {esql} query runs on. - -[source,console] ----- -POST /_query?format=txt -{ - "query": """ - FROM library - | KEEP author, name, page_count, release_date - | SORT page_count DESC - | LIMIT 5 - """, - "filter": { - "range": { - "page_count": { - "gte": 100, - "lte": 200 - } - } - } -} ----- -// TEST[setup:library] - -Which returns: - -[source,text] --------------------------------------------------- - author | name | page_count | release_date ----------------+------------------------------------+---------------+------------------------ -Douglas Adams |The Hitchhiker's Guide to the Galaxy|180 |1979-10-12T00:00:00.000Z --------------------------------------------------- -// TESTRESPONSE[s/\|/\\|/ s/\+/\\+/] -// TESTRESPONSE[non_json] - -[discrete] -[[esql-rest-columnar]] -=== Columnar results - -By default, {esql} returns results as rows. For example, `FROM` returns each -individual document as one row. For the `json`, `yaml`, `cbor` and `smile` -<>, {esql} can return the results in a columnar -fashion where one row represents all the values of a certain column in the -results. - -[source,console] ----- -POST /_query?format=json -{ - "query": """ - FROM library - | KEEP author, name, page_count, release_date - | SORT page_count DESC - | LIMIT 5 - """, - "columnar": true -} ----- -// TEST[setup:library] - -Which returns: - -[source,console-result] ----- -{ - "columns": [ - {"name": "author", "type": "text"}, - {"name": "name", "type": "text"}, - {"name": "page_count", "type": "integer"}, - {"name": "release_date", "type": "date"} - ], - "values": [ - ["Peter F. Hamilton", "Vernor Vinge", "Frank Herbert", "Alastair Reynolds", "James S.A. Corey"], - ["Pandora's Star", "A Fire Upon the Deep", "Dune", "Revelation Space", "Leviathan Wakes"], - [768, 613, 604, 585, 561], - ["2004-03-02T00:00:00.000Z", "1992-06-01T00:00:00.000Z", "1965-06-01T00:00:00.000Z", "2000-03-15T00:00:00.000Z", "2011-06-02T00:00:00.000Z"] - ] -} ----- - -[discrete] -[[esql-rest-params]] -=== Passing parameters to a query - -Values, for example for a condition, can be passed to a query "inline", by -integrating the value in the query string itself: - -[source,console] ----- -POST /_query -{ - "query": """ - FROM library - | EVAL year = DATE_EXTRACT("year", release_date) - | WHERE page_count > 300 AND author == "Frank Herbert" - | STATS count = COUNT(*) by year - | WHERE count > 0 - | LIMIT 5 - """ -} ----- -// TEST[setup:library] - -To avoid any attempts of hacking or code injection, extract the values in a -separate list of parameters. Use question mark placeholders (`?`) in the query -string for each of the parameters: - -[source,console] ----- -POST /_query -{ - "query": """ - FROM library - | EVAL year = DATE_EXTRACT("year", release_date) - | WHERE page_count > ? AND author == ? - | STATS count = COUNT(*) by year - | WHERE count > ? - | LIMIT 5 - """, - "params": [300, "Frank Herbert", 0] -} ----- -// TEST[setup:library] diff --git a/x-pack/plugins/elastic_assistant/server/knowledge_base/esql/documentation/esql_syntax.asciidoc b/x-pack/plugins/elastic_assistant/server/knowledge_base/esql/documentation/esql_syntax.asciidoc deleted file mode 100644 index 725b1d3ff1e03..0000000000000 --- a/x-pack/plugins/elastic_assistant/server/knowledge_base/esql/documentation/esql_syntax.asciidoc +++ /dev/null @@ -1,90 +0,0 @@ -[[esql-syntax]] -=== {esql} syntax reference - -++++ -Syntax reference -++++ - -[discrete] -[[esql-basic-syntax]] -=== Basic syntax - -An {esql} query is composed of a <> followed -by an optional series of <>, -separated by a pipe character: `|`. For example: - -[source,esql] ----- -source-command -| processing-command1 -| processing-command2 ----- - -The result of a query is the table produced by the final processing command. - -For an overview of all supported commands, functions, and operators, refer to <> and <>. - -[NOTE] -==== -For readability, this documentation puts each processing command on a new -line. However, you can write an {esql} query as a single line. The following -query is identical to the previous one: - -[source,esql] ----- -source-command | processing-command1 | processing-command2 ----- -==== - -[discrete] -[[esql-comments]] -==== Comments -{esql} uses C++ style comments: - -* double slash `//` for single line comments -* `/*` and `*/` for block comments - -[source,esql] ----- -// Query the employees index -FROM employees -| WHERE height > 2 ----- - -[source,esql] ----- -FROM /* Query the employees index */ employees -| WHERE height > 2 ----- - -[source,esql] ----- -FROM employees -/* Query the - * employees - * index */ -| WHERE height > 2 ----- - -[discrete] -[[esql-timespan-literals]] -==== Timespan literals - -Datetime intervals and timespans can be expressed using timespan literals. -Timespan literals are a combination of a number and a qualifier. These -qualifiers are supported: - -* `millisecond`/`milliseconds` -* `second`/`seconds` -* `minute`/`minutes` -* `hour`/`hours` -* `day`/`days` -* `week`/`weeks` -* `month`/`months` -* `year`/`years` - -Timespan literals are not whitespace sensitive. These expressions are all valid: - -* `1day` -* `1 day` -* `1 day` diff --git a/x-pack/plugins/elastic_assistant/server/knowledge_base/esql/documentation/functions/abs.asciidoc b/x-pack/plugins/elastic_assistant/server/knowledge_base/esql/documentation/functions/abs.asciidoc deleted file mode 100644 index 3adb7dff07043..0000000000000 --- a/x-pack/plugins/elastic_assistant/server/knowledge_base/esql/documentation/functions/abs.asciidoc +++ /dev/null @@ -1,18 +0,0 @@ -[discrete] -[[esql-abs]] -=== `ABS` -[.text-center] -image::esql/functions/signature/abs.svg[Embedded,opts=inline] - -Returns the absolute value. - -[source,esql] ----- -FROM employees -| KEEP first_name, last_name, height -| EVAL abs_height = ABS(0.0 - height) ----- - -Supported types: - -include::types/abs.asciidoc[] diff --git a/x-pack/plugins/elastic_assistant/server/knowledge_base/esql/documentation/functions/acos.asciidoc b/x-pack/plugins/elastic_assistant/server/knowledge_base/esql/documentation/functions/acos.asciidoc deleted file mode 100644 index e4d04bd169c78..0000000000000 --- a/x-pack/plugins/elastic_assistant/server/knowledge_base/esql/documentation/functions/acos.asciidoc +++ /dev/null @@ -1,33 +0,0 @@ -[discrete] -[[esql-acos]] -=== `ACOS` - -*Syntax* - -[.text-center] -image::esql/functions/signature/acos.svg[Embedded,opts=inline] - -*Parameters* - -`n`:: -Numeric expression. If `null`, the function returns `null`. - -*Description* - -Returns the {wikipedia}/Inverse_trigonometric_functions[arccosine] of `n` as an -angle, expressed in radians. - -*Supported types* - -include::types/acos.asciidoc[] - -*Example* - -[source.merge.styled,esql] ----- -include::{esql-specs}/floats.csv-spec[tag=acos] ----- -[%header.monospaced.styled,format=dsv,separator=|] -|=== -include::{esql-specs}/floats.csv-spec[tag=acos-result] -|=== diff --git a/x-pack/plugins/elastic_assistant/server/knowledge_base/esql/documentation/functions/aggregation_functions.asciidoc b/x-pack/plugins/elastic_assistant/server/knowledge_base/esql/documentation/functions/aggregation_functions.asciidoc deleted file mode 100644 index bd501ea49f158..0000000000000 --- a/x-pack/plugins/elastic_assistant/server/knowledge_base/esql/documentation/functions/aggregation_functions.asciidoc +++ /dev/null @@ -1,30 +0,0 @@ -[[esql-agg-functions]] -==== {esql} aggregate functions - -++++ -Aggregate functions -++++ - -The <> function supports these aggregate functions: - -// tag::agg_list[] -* <> -* <> -* <> -* <> -* <> -* <> -* <> -* <> -* <> -// end::agg_list[] - -include::avg.asciidoc[] -include::count.asciidoc[] -include::count-distinct.asciidoc[] -include::max.asciidoc[] -include::median.asciidoc[] -include::median-absolute-deviation.asciidoc[] -include::min.asciidoc[] -include::percentile.asciidoc[] -include::sum.asciidoc[] diff --git a/x-pack/plugins/elastic_assistant/server/knowledge_base/esql/documentation/functions/asin.asciidoc b/x-pack/plugins/elastic_assistant/server/knowledge_base/esql/documentation/functions/asin.asciidoc deleted file mode 100644 index f03b5276b7dd6..0000000000000 --- a/x-pack/plugins/elastic_assistant/server/knowledge_base/esql/documentation/functions/asin.asciidoc +++ /dev/null @@ -1,20 +0,0 @@ -[discrete] -[[esql-asin]] -=== `ASIN` -[.text-center] -image::esql/functions/signature/asin.svg[Embedded,opts=inline] - -Inverse https://en.wikipedia.org/wiki/Inverse_trigonometric_functions[sine] trigonometric function. - -[source.merge.styled,esql] ----- -include::{esql-specs}/floats.csv-spec[tag=asin] ----- -[%header.monospaced.styled,format=dsv,separator=|] -|=== -include::{esql-specs}/floats.csv-spec[tag=asin-result] -|=== - -Supported types: - -include::types/asin.asciidoc[] diff --git a/x-pack/plugins/elastic_assistant/server/knowledge_base/esql/documentation/functions/atan.asciidoc b/x-pack/plugins/elastic_assistant/server/knowledge_base/esql/documentation/functions/atan.asciidoc deleted file mode 100644 index 3813e096aeba1..0000000000000 --- a/x-pack/plugins/elastic_assistant/server/knowledge_base/esql/documentation/functions/atan.asciidoc +++ /dev/null @@ -1,20 +0,0 @@ -[discrete] -[[esql-atan]] -=== `ATAN` -[.text-center] -image::esql/functions/signature/atan.svg[Embedded,opts=inline] - -Inverse https://en.wikipedia.org/wiki/Inverse_trigonometric_functions[tangent] trigonometric function. - -[source.merge.styled,esql] ----- -include::{esql-specs}/floats.csv-spec[tag=atan] ----- -[%header.monospaced.styled,format=dsv,separator=|] -|=== -include::{esql-specs}/floats.csv-spec[tag=atan-result] -|=== - -Supported types: - -include::types/atan.asciidoc[] diff --git a/x-pack/plugins/elastic_assistant/server/knowledge_base/esql/documentation/functions/atan2.asciidoc b/x-pack/plugins/elastic_assistant/server/knowledge_base/esql/documentation/functions/atan2.asciidoc deleted file mode 100644 index e78a219333344..0000000000000 --- a/x-pack/plugins/elastic_assistant/server/knowledge_base/esql/documentation/functions/atan2.asciidoc +++ /dev/null @@ -1,21 +0,0 @@ -[discrete] -[[esql-atan2]] -=== `ATAN2` -[.text-center] -image::esql/functions/signature/atan2.svg[Embedded,opts=inline] - -The https://en.wikipedia.org/wiki/Atan2[angle] between the positive x-axis and the -ray from the origin to the point (x , y) in the Cartesian plane. - -[source.merge.styled,esql] ----- -include::{esql-specs}/floats.csv-spec[tag=atan2] ----- -[%header.monospaced.styled,format=dsv,separator=|] -|=== -include::{esql-specs}/floats.csv-spec[tag=atan2-result] -|=== - -Supported types: - -include::types/atan2.asciidoc[] diff --git a/x-pack/plugins/elastic_assistant/server/knowledge_base/esql/documentation/functions/auto_bucket.asciidoc b/x-pack/plugins/elastic_assistant/server/knowledge_base/esql/documentation/functions/auto_bucket.asciidoc deleted file mode 100644 index 47e453f382229..0000000000000 --- a/x-pack/plugins/elastic_assistant/server/knowledge_base/esql/documentation/functions/auto_bucket.asciidoc +++ /dev/null @@ -1,72 +0,0 @@ -[discrete] -[[esql-auto_bucket]] -=== `AUTO_BUCKET` -Creates human-friendly buckets and returns a `datetime` value for each row that -corresponds to the resulting bucket the row falls into. Combine `AUTO_BUCKET` -with <> to create a date histogram. - -You provide a target number of buckets, a start date, and an end date, and it -picks an appropriate bucket size to generate the target number of buckets or -fewer. For example, this asks for at most 20 buckets over a whole year, which -picks monthly buckets: - -[source.merge.styled,esql] ----- -include::{esql-specs}/date.csv-spec[tag=auto_bucket_month] ----- -[%header.monospaced.styled,format=dsv,separator=|] -|=== -include::{esql-specs}/date.csv-spec[tag=auto_bucket_month-result] -|=== - -The goal isn't to provide *exactly* the target number of buckets, it's to pick a -range that people are comfortable with that provides at most the target number of -buckets. - -If you ask for more buckets then `AUTO_BUCKET` can pick a smaller range. For example, -asking for at most 100 buckets in a year will get you week long buckets: - -[source.merge.styled,esql] ----- -include::{esql-specs}/date.csv-spec[tag=auto_bucket_week] ----- -[%header.monospaced.styled,format=dsv,separator=|] -|=== -include::{esql-specs}/date.csv-spec[tag=auto_bucket_week-result] -|=== - -`AUTO_BUCKET` does not filter any rows. It only uses the provided time range to -pick a good bucket size. For rows with a date outside of the range, it returns a -`datetime` that corresponds to a bucket outside the range. Combine `AUTO_BUCKET` -with <> to filter rows. - -A more complete example might look like: - -[source.merge.styled,esql] ----- -include::{esql-specs}/date.csv-spec[tag=auto_bucket_in_agg] ----- -[%header.monospaced.styled,format=dsv,separator=|] -|=== -include::{esql-specs}/date.csv-spec[tag=auto_bucket_in_agg-result] -|=== - -NOTE: `AUTO_BUCKET` does not create buckets that don't match any documents. That's -why the example above is missing `1985-03-01` and other dates. - -==== Numeric fields - -`auto_bucket` can also operate on numeric fields like this: -[source.merge.styled,esql] ----- -include::{esql-specs}/ints.csv-spec[tag=auto_bucket] ----- -[%header.monospaced.styled,format=dsv,separator=|] -|=== -include::{esql-specs}/ints.csv-spec[tag=auto_bucket-result] -|=== - -Unlike the example above where you are intentionally filtering on a date range, -you rarely want to filter on a numeric range. So you have find the `min` and `max` -separately. We don't yet have an easy way to do that automatically. Improvements -coming! diff --git a/x-pack/plugins/elastic_assistant/server/knowledge_base/esql/documentation/functions/avg.asciidoc b/x-pack/plugins/elastic_assistant/server/knowledge_base/esql/documentation/functions/avg.asciidoc deleted file mode 100644 index 972d30545ceb4..0000000000000 --- a/x-pack/plugins/elastic_assistant/server/knowledge_base/esql/documentation/functions/avg.asciidoc +++ /dev/null @@ -1,15 +0,0 @@ -[discrete] -[[esql-agg-avg]] -=== `AVG` -The average of a numeric field. - -[source.merge.styled,esql] ----- -include::{esql-specs}/stats.csv-spec[tag=avg] ----- -[%header.monospaced.styled,format=dsv,separator=|] -|=== -include::{esql-specs}/stats.csv-spec[tag=avg-result] -|=== - -The result is always a `double` not matter the input type. diff --git a/x-pack/plugins/elastic_assistant/server/knowledge_base/esql/documentation/functions/binary.asciidoc b/x-pack/plugins/elastic_assistant/server/knowledge_base/esql/documentation/functions/binary.asciidoc deleted file mode 100644 index ba93f57af7ad6..0000000000000 --- a/x-pack/plugins/elastic_assistant/server/knowledge_base/esql/documentation/functions/binary.asciidoc +++ /dev/null @@ -1,12 +0,0 @@ -[discrete] -[[esql-binary-operators]] -=== Binary operators - -These binary comparison operators are supported: - -* equality: `==` -* inequality: `!=` -* less than: `<` -* less than or equal: `<=` -* larger than: `>` -* larger than or equal: `>=` \ No newline at end of file diff --git a/x-pack/plugins/elastic_assistant/server/knowledge_base/esql/documentation/functions/case.asciidoc b/x-pack/plugins/elastic_assistant/server/knowledge_base/esql/documentation/functions/case.asciidoc deleted file mode 100644 index b243adf875cb4..0000000000000 --- a/x-pack/plugins/elastic_assistant/server/knowledge_base/esql/documentation/functions/case.asciidoc +++ /dev/null @@ -1,42 +0,0 @@ -[discrete] -[[esql-case]] -=== `CASE` - -*Syntax* - -[source,txt] ----- -CASE(condition1, value1[, ..., conditionN, valueN][, default_value]) ----- - -*Parameters* - -`conditionX`:: -A condition. - -`valueX`:: -The value that's returned when the corresponding condition is the first to -evaluate to `true`. - -`default_value`:: -The default value that's is returned when no condition matches. - -*Description* - -Accepts pairs of conditions and values. The function returns the value that -belongs to the first condition that evaluates to `true`. - -If the number of arguments is odd, the last argument is the default value which -is returned when no condition matches. - -*Example* - -[source,esql] -[source.merge.styled,esql] ----- -include::{esql-specs}/docs.csv-spec[tag=case] ----- -[%header.monospaced.styled,format=dsv,separator=|] -|=== -include::{esql-specs}/docs.csv-spec[tag=case-result] -|=== diff --git a/x-pack/plugins/elastic_assistant/server/knowledge_base/esql/documentation/functions/ceil.asciidoc b/x-pack/plugins/elastic_assistant/server/knowledge_base/esql/documentation/functions/ceil.asciidoc deleted file mode 100644 index f977e544e6c3f..0000000000000 --- a/x-pack/plugins/elastic_assistant/server/knowledge_base/esql/documentation/functions/ceil.asciidoc +++ /dev/null @@ -1,24 +0,0 @@ -[discrete] -[[esql-ceil]] -=== `CEIL` -[.text-center] -image::esql/functions/signature/ceil.svg[Embedded,opts=inline] - -Round a number up to the nearest integer. - -[source.merge.styled,esql] ----- -include::{esql-specs}/math.csv-spec[tag=ceil] ----- -[%header.monospaced.styled,format=dsv,separator=|] -|=== -include::{esql-specs}/math.csv-spec[tag=ceil-result] -|=== - -NOTE: This is a noop for `long` (including unsigned) and `integer`. - For `double` this picks the the closest `double` value to the integer ala - {javadoc}/java.base/java/lang/Math.html#ceil(double)[Math.ceil]. - -Supported types: - -include::types/ceil.asciidoc[] diff --git a/x-pack/plugins/elastic_assistant/server/knowledge_base/esql/documentation/functions/cidr_match.asciidoc b/x-pack/plugins/elastic_assistant/server/knowledge_base/esql/documentation/functions/cidr_match.asciidoc deleted file mode 100644 index 5072a6eef7fd5..0000000000000 --- a/x-pack/plugins/elastic_assistant/server/knowledge_base/esql/documentation/functions/cidr_match.asciidoc +++ /dev/null @@ -1,16 +0,0 @@ -[discrete] -[[esql-cidr_match]] -=== `CIDR_MATCH` - -Returns `true` if the provided IP is contained in one of the provided CIDR -blocks. - -`CIDR_MATCH` accepts two or more arguments. The first argument is the IP -address of type `ip` (both IPv4 and IPv6 are supported). Subsequent arguments -are the CIDR blocks to test the IP against. - -[source,esql] ----- -FROM hosts -| WHERE CIDR_MATCH(ip, "127.0.0.2/32", "127.0.0.3/32") ----- diff --git a/x-pack/plugins/elastic_assistant/server/knowledge_base/esql/documentation/functions/coalesce.asciidoc b/x-pack/plugins/elastic_assistant/server/knowledge_base/esql/documentation/functions/coalesce.asciidoc deleted file mode 100644 index 550780eaa070d..0000000000000 --- a/x-pack/plugins/elastic_assistant/server/knowledge_base/esql/documentation/functions/coalesce.asciidoc +++ /dev/null @@ -1,14 +0,0 @@ -[discrete] -[[esql-coalesce]] -=== `COALESCE` - -Returns the first non-null value. - -[source.merge.styled,esql] ----- -include::{esql-specs}/null.csv-spec[tag=coalesce] ----- -[%header.monospaced.styled,format=dsv,separator=|] -|=== -include::{esql-specs}/null.csv-spec[tag=coalesce-result] -|=== diff --git a/x-pack/plugins/elastic_assistant/server/knowledge_base/esql/documentation/functions/concat.asciidoc b/x-pack/plugins/elastic_assistant/server/knowledge_base/esql/documentation/functions/concat.asciidoc deleted file mode 100644 index 4864f5623a170..0000000000000 --- a/x-pack/plugins/elastic_assistant/server/knowledge_base/esql/documentation/functions/concat.asciidoc +++ /dev/null @@ -1,11 +0,0 @@ -[discrete] -[[esql-concat]] -=== `CONCAT` -Concatenates two or more strings. - -[source,esql] ----- -FROM employees -| KEEP first_name, last_name, height -| EVAL fullname = CONCAT(first_name, " ", last_name) ----- diff --git a/x-pack/plugins/elastic_assistant/server/knowledge_base/esql/documentation/functions/conditional_functions_and_expressions.asciidoc b/x-pack/plugins/elastic_assistant/server/knowledge_base/esql/documentation/functions/conditional_functions_and_expressions.asciidoc deleted file mode 100644 index d835a14856c03..0000000000000 --- a/x-pack/plugins/elastic_assistant/server/knowledge_base/esql/documentation/functions/conditional_functions_and_expressions.asciidoc +++ /dev/null @@ -1,21 +0,0 @@ -[[esql-conditional-functions-and-expressions]] -==== {esql} conditional functions and expressions - -++++ -Conditional functions and expressions -++++ - -Conditional functions return one of their arguments by evaluating in an if-else -manner. {esql} supports these conditional functions: - -// tag::cond_list[] -* <> -* <> -* <> -* <> -// end::cond_list[] - -include::case.asciidoc[] -include::coalesce.asciidoc[] -include::greatest.asciidoc[] -include::least.asciidoc[] diff --git a/x-pack/plugins/elastic_assistant/server/knowledge_base/esql/documentation/functions/cos.asciidoc b/x-pack/plugins/elastic_assistant/server/knowledge_base/esql/documentation/functions/cos.asciidoc deleted file mode 100644 index 5dcbb7bea37f4..0000000000000 --- a/x-pack/plugins/elastic_assistant/server/knowledge_base/esql/documentation/functions/cos.asciidoc +++ /dev/null @@ -1,20 +0,0 @@ -[discrete] -[[esql-cos]] -=== `COS` -[.text-center] -image::esql/functions/signature/cos.svg[Embedded,opts=inline] - -https://en.wikipedia.org/wiki/Sine_and_cosine[Cosine] trigonometric function. - -[source.merge.styled,esql] ----- -include::{esql-specs}/floats.csv-spec[tag=cos] ----- -[%header.monospaced.styled,format=dsv,separator=|] -|=== -include::{esql-specs}/floats.csv-spec[tag=cos-result] -|=== - -Supported types: - -include::types/cos.asciidoc[] diff --git a/x-pack/plugins/elastic_assistant/server/knowledge_base/esql/documentation/functions/cosh.asciidoc b/x-pack/plugins/elastic_assistant/server/knowledge_base/esql/documentation/functions/cosh.asciidoc deleted file mode 100644 index 7bf0840958655..0000000000000 --- a/x-pack/plugins/elastic_assistant/server/knowledge_base/esql/documentation/functions/cosh.asciidoc +++ /dev/null @@ -1,20 +0,0 @@ -[discrete] -[[esql-cosh]] -=== `COSH` -[.text-center] -image::esql/functions/signature/cosh.svg[Embedded,opts=inline] - -https://en.wikipedia.org/wiki/Hyperbolic_functions[Cosine] hyperbolic function. - -[source.merge.styled,esql] ----- -include::{esql-specs}/floats.csv-spec[tag=cosh] ----- -[%header.monospaced.styled,format=dsv,separator=|] -|=== -include::{esql-specs}/floats.csv-spec[tag=cosh-result] -|=== - -Supported types: - -include::types/cosh.asciidoc[] diff --git a/x-pack/plugins/elastic_assistant/server/knowledge_base/esql/documentation/functions/count.asciidoc b/x-pack/plugins/elastic_assistant/server/knowledge_base/esql/documentation/functions/count.asciidoc deleted file mode 100644 index a148df07edb4d..0000000000000 --- a/x-pack/plugins/elastic_assistant/server/knowledge_base/esql/documentation/functions/count.asciidoc +++ /dev/null @@ -1,27 +0,0 @@ -[discrete] -[[esql-agg-count]] -=== `COUNT` -Counts field values. - -[source.merge.styled,esql] ----- -include::{esql-specs}/stats.csv-spec[tag=count] ----- -[%header.monospaced.styled,format=dsv,separator=|] -|=== -include::{esql-specs}/stats.csv-spec[tag=count-result] -|=== - -Can take any field type as input and the result is always a `long` not matter -the input type. - -To count the number of rows, use `COUNT(*)`: - -[source.merge.styled,esql] ----- -include::{esql-specs}/docs.csv-spec[tag=countAll] ----- -[%header.monospaced.styled,format=dsv,separator=|] -|=== -include::{esql-specs}/docs.csv-spec[tag=countAll-result] -|=== \ No newline at end of file diff --git a/x-pack/plugins/elastic_assistant/server/knowledge_base/esql/documentation/functions/count_distinct.asciidoc b/x-pack/plugins/elastic_assistant/server/knowledge_base/esql/documentation/functions/count_distinct.asciidoc deleted file mode 100644 index b5b1659140f63..0000000000000 --- a/x-pack/plugins/elastic_assistant/server/knowledge_base/esql/documentation/functions/count_distinct.asciidoc +++ /dev/null @@ -1,46 +0,0 @@ -[discrete] -[[esql-agg-count-distinct]] -=== `COUNT_DISTINCT` -The approximate number of distinct values. - -[source.merge.styled,esql] ----- -include::{esql-specs}/stats_count_distinct.csv-spec[tag=count-distinct] ----- -[%header.monospaced.styled,format=dsv,separator=|] -|=== -include::{esql-specs}/stats_count_distinct.csv-spec[tag=count-distinct-result] -|=== - -Can take any field type as input and the result is always a `long` not matter -the input type. - -[discrete] -==== Counts are approximate - -Computing exact counts requires loading values into a set and returning its -size. This doesn't scale when working on high-cardinality sets and/or large -values as the required memory usage and the need to communicate those -per-shard sets between nodes would utilize too many resources of the cluster. - -This `COUNT_DISTINCT` function is based on the -https://static.googleusercontent.com/media/research.google.com/fr//pubs/archive/40671.pdf[HyperLogLog++] -algorithm, which counts based on the hashes of the values with some interesting -properties: - -include::../../aggregations/metrics/cardinality-aggregation.asciidoc[tag=explanation] - -[discrete] -==== Precision is configurable - -The `COUNT_DISTINCT` function takes an optional second parameter to configure the -precision discussed previously. - -[source.merge.styled,esql] ----- -include::{esql-specs}/stats_count_distinct.csv-spec[tag=count-distinct-precision] ----- -[%header.monospaced.styled,format=dsv,separator=|] -|=== -include::{esql-specs}/stats_count_distinct.csv-spec[tag=count-distinct-precision-result] -|=== diff --git a/x-pack/plugins/elastic_assistant/server/knowledge_base/esql/documentation/functions/date_extract.asciidoc b/x-pack/plugins/elastic_assistant/server/knowledge_base/esql/documentation/functions/date_extract.asciidoc deleted file mode 100644 index 89ef1cf261094..0000000000000 --- a/x-pack/plugins/elastic_assistant/server/knowledge_base/esql/documentation/functions/date_extract.asciidoc +++ /dev/null @@ -1,15 +0,0 @@ -[discrete] -[[esql-date_extract]] -=== `DATE_EXTRACT` -Extracts parts of a date, like year, month, day, hour. -The supported field types are those provided by https://docs.oracle.com/javase/8/docs/api/java/time/temporal/ChronoField.html[java.time.temporal.ChronoField]. - -[source.merge.styled,esql] ----- -include::{esql-specs}/docs.csv-spec[tag=dateExtract] ----- -[%header.monospaced.styled,format=dsv,separator=|] -|=== -include::{esql-specs}/docs.csv-spec[tag=dateExtract-result] -|=== - diff --git a/x-pack/plugins/elastic_assistant/server/knowledge_base/esql/documentation/functions/date_format.asciidoc b/x-pack/plugins/elastic_assistant/server/knowledge_base/esql/documentation/functions/date_format.asciidoc deleted file mode 100644 index 5a87f31412cc8..0000000000000 --- a/x-pack/plugins/elastic_assistant/server/knowledge_base/esql/documentation/functions/date_format.asciidoc +++ /dev/null @@ -1,12 +0,0 @@ -[discrete] -[[esql-date_format]] -=== `DATE_FORMAT` -Returns a string representation of a date in the provided format. If no format -is specified, the `yyyy-MM-dd'T'HH:mm:ss.SSSZ` format is used. - -[source,esql] ----- -FROM employees -| KEEP first_name, last_name, hire_date -| EVAL hired = DATE_FORMAT("YYYY-MM-dd", hire_date) ----- diff --git a/x-pack/plugins/elastic_assistant/server/knowledge_base/esql/documentation/functions/date_parse.asciidoc b/x-pack/plugins/elastic_assistant/server/knowledge_base/esql/documentation/functions/date_parse.asciidoc deleted file mode 100644 index c74656ff1dbd7..0000000000000 --- a/x-pack/plugins/elastic_assistant/server/knowledge_base/esql/documentation/functions/date_parse.asciidoc +++ /dev/null @@ -1,37 +0,0 @@ -[discrete] -[[esql-date_parse]] -=== `DATE_PARSE` - -*Syntax* - -[source,txt] ----- -DATE_PARSE([format,] date_string) ----- - -*Parameters* - -`format`:: -The date format. Refer to the -https://docs.oracle.com/en/java/javase/14/docs/api/java.base/java/time/format/DateTimeFormatter.html[`DateTimeFormatter` -documentation] for the syntax. If `null`, the function returns `null`. - -`date_string`:: -Date expression as a string. If `null` or an empty string, the function returns -`null`. - -*Description* - -Returns a date by parsing the second argument using the format specified in the -first argument. - -*Example* - -[source.merge.styled,esql] ----- -include::{esql-specs}/docs.csv-spec[tag=dateParse] ----- -[%header.monospaced.styled,format=dsv,separator=|] -|=== -include::{esql-specs}/docs.csv-spec[tag=dateParse-result] -|=== diff --git a/x-pack/plugins/elastic_assistant/server/knowledge_base/esql/documentation/functions/date_time_functions.asciidoc b/x-pack/plugins/elastic_assistant/server/knowledge_base/esql/documentation/functions/date_time_functions.asciidoc deleted file mode 100644 index 8ff7b1e974eeb..0000000000000 --- a/x-pack/plugins/elastic_assistant/server/knowledge_base/esql/documentation/functions/date_time_functions.asciidoc +++ /dev/null @@ -1,24 +0,0 @@ -[[esql-date-time-functions]] -==== {esql} date-time functions - -++++ -Date-time functions -++++ - -{esql} supports these date-time functions: - -// tag::date_list[] -* <> -* <> -* <> -* <> -* <> -* <> -// end::date_list[] - -include::auto_bucket.asciidoc[] -include::date_extract.asciidoc[] -include::date_format.asciidoc[] -include::date_parse.asciidoc[] -include::date_trunc.asciidoc[] -include::now.asciidoc[] diff --git a/x-pack/plugins/elastic_assistant/server/knowledge_base/esql/documentation/functions/date_trunc.asciidoc b/x-pack/plugins/elastic_assistant/server/knowledge_base/esql/documentation/functions/date_trunc.asciidoc deleted file mode 100644 index cacfefe73d0fd..0000000000000 --- a/x-pack/plugins/elastic_assistant/server/knowledge_base/esql/documentation/functions/date_trunc.asciidoc +++ /dev/null @@ -1,13 +0,0 @@ -[discrete] -[[esql-date_trunc]] -=== `DATE_TRUNC` -Rounds down a date to the closest interval. Intervals can be expressed using the -<>. - -[source,esql] ----- -FROM employees -| EVAL year_hired = DATE_TRUNC(1 year, hire_date) -| STATS count(emp_no) BY year_hired -| SORT year_hired ----- diff --git a/x-pack/plugins/elastic_assistant/server/knowledge_base/esql/documentation/functions/e.asciidoc b/x-pack/plugins/elastic_assistant/server/knowledge_base/esql/documentation/functions/e.asciidoc deleted file mode 100644 index 56bf97fd01740..0000000000000 --- a/x-pack/plugins/elastic_assistant/server/knowledge_base/esql/documentation/functions/e.asciidoc +++ /dev/null @@ -1,16 +0,0 @@ -[discrete] -[[esql-e]] -=== `E` -[.text-center] -image::esql/functions/signature/e.svg[Embedded,opts=inline] - -{wikipedia}/E_(mathematical_constant)[Euler's number]. - -[source.merge.styled,esql] ----- -include::{esql-specs}/math.csv-spec[tag=e] ----- -[%header.monospaced.styled,format=dsv,separator=|] -|=== -include::{esql-specs}/math.csv-spec[tag=e-result] -|=== diff --git a/x-pack/plugins/elastic_assistant/server/knowledge_base/esql/documentation/functions/ends_with.asciidoc b/x-pack/plugins/elastic_assistant/server/knowledge_base/esql/documentation/functions/ends_with.asciidoc deleted file mode 100644 index fd2d99931163a..0000000000000 --- a/x-pack/plugins/elastic_assistant/server/knowledge_base/esql/documentation/functions/ends_with.asciidoc +++ /dev/null @@ -1,21 +0,0 @@ -[discrete] -[[esql-ends_with]] -=== `ENDS_WITH` -[.text-center] -image::esql/functions/signature/ends_with.svg[Embedded,opts=inline] - -Returns a boolean that indicates whether a keyword string ends with another -string: - -[source.merge.styled,esql] ----- -include::{esql-specs}/string.csv-spec[tag=endsWith] ----- -[%header.monospaced.styled,format=dsv,separator=|] -|=== -include::{esql-specs}/string.csv-spec[tag=endsWith-result] -|=== - -Supported types: - -include::types/ends_with.asciidoc[] diff --git a/x-pack/plugins/elastic_assistant/server/knowledge_base/esql/documentation/functions/floor.asciidoc b/x-pack/plugins/elastic_assistant/server/knowledge_base/esql/documentation/functions/floor.asciidoc deleted file mode 100644 index 109033bb18827..0000000000000 --- a/x-pack/plugins/elastic_assistant/server/knowledge_base/esql/documentation/functions/floor.asciidoc +++ /dev/null @@ -1,24 +0,0 @@ -[discrete] -[[esql-floor]] -=== `FLOOR` -[.text-center] -image::esql/functions/signature/floor.svg[Embedded,opts=inline] - -Round a number down to the nearest integer. - -[source.merge.styled,esql] ----- -include::{esql-specs}/math.csv-spec[tag=floor] ----- -[%header.monospaced.styled,format=dsv,separator=|] -|=== -include::{esql-specs}/math.csv-spec[tag=floor-result] -|=== - -NOTE: This is a noop for `long` (including unsigned) and `integer`. - For `double` this picks the the closest `double` value to the integer ala - {javadoc}/java.base/java/lang/Math.html#floor(double)[Math.floor]. - -Supported types: - -include::types/floor.asciidoc[] diff --git a/x-pack/plugins/elastic_assistant/server/knowledge_base/esql/documentation/functions/greatest.asciidoc b/x-pack/plugins/elastic_assistant/server/knowledge_base/esql/documentation/functions/greatest.asciidoc deleted file mode 100644 index 24dd08de2819c..0000000000000 --- a/x-pack/plugins/elastic_assistant/server/knowledge_base/esql/documentation/functions/greatest.asciidoc +++ /dev/null @@ -1,25 +0,0 @@ -[discrete] -[[esql-greatest]] -=== `GREATEST` -[.text-center] -image::esql/functions/signature/greatest.svg[Embedded,opts=inline] - -Returns the maximum value from many columns. This is similar to <> -except it's intended to run on multiple columns at once. - -[source.merge.styled,esql] ----- -include::{esql-specs}/math.csv-spec[tag=greatest] ----- -[%header.monospaced.styled,format=dsv,separator=|] -|=== -include::{esql-specs}/math.csv-spec[tag=greatest-result] -|=== - -NOTE: When run on `keyword` or `text` fields, this'll return the last string - in alphabetical order. When run on `boolean` columns this will return - `true` if any values are `true`. - -Supported types: - -include::types/greatest.asciidoc[] diff --git a/x-pack/plugins/elastic_assistant/server/knowledge_base/esql/documentation/functions/in.asciidoc b/x-pack/plugins/elastic_assistant/server/knowledge_base/esql/documentation/functions/in.asciidoc deleted file mode 100644 index be5688250ecc7..0000000000000 --- a/x-pack/plugins/elastic_assistant/server/knowledge_base/esql/documentation/functions/in.asciidoc +++ /dev/null @@ -1,11 +0,0 @@ -[discrete] -[[esql-in-operator]] -=== `IN` - -The `IN` operator allows testing whether a field or expression equals -an element in a list of literals, fields or expressions: - -[source,esql] ----- -include::{esql-specs}/row.csv-spec[tag=in-with-expressions] ----- \ No newline at end of file diff --git a/x-pack/plugins/elastic_assistant/server/knowledge_base/esql/documentation/functions/is_finite.asciidoc b/x-pack/plugins/elastic_assistant/server/knowledge_base/esql/documentation/functions/is_finite.asciidoc deleted file mode 100644 index f7b7ad73a3952..0000000000000 --- a/x-pack/plugins/elastic_assistant/server/knowledge_base/esql/documentation/functions/is_finite.asciidoc +++ /dev/null @@ -1,10 +0,0 @@ -[discrete] -[[esql-is_finite]] -=== `IS_FINITE` -Returns a boolean that indicates whether its input is a finite number. - -[source,esql] ----- -ROW d = 1.0 -| EVAL s = IS_FINITE(d/0) ----- diff --git a/x-pack/plugins/elastic_assistant/server/knowledge_base/esql/documentation/functions/is_infinite.asciidoc b/x-pack/plugins/elastic_assistant/server/knowledge_base/esql/documentation/functions/is_infinite.asciidoc deleted file mode 100644 index 56158a786c020..0000000000000 --- a/x-pack/plugins/elastic_assistant/server/knowledge_base/esql/documentation/functions/is_infinite.asciidoc +++ /dev/null @@ -1,10 +0,0 @@ -[discrete] -[[esql-is_infinite]] -=== `IS_INFINITE` -Returns a boolean that indicates whether its input is infinite. - -[source,esql] ----- -ROW d = 1.0 -| EVAL s = IS_INFINITE(d/0) ----- diff --git a/x-pack/plugins/elastic_assistant/server/knowledge_base/esql/documentation/functions/is_nan.asciidoc b/x-pack/plugins/elastic_assistant/server/knowledge_base/esql/documentation/functions/is_nan.asciidoc deleted file mode 100644 index 25b50a9e96bba..0000000000000 --- a/x-pack/plugins/elastic_assistant/server/knowledge_base/esql/documentation/functions/is_nan.asciidoc +++ /dev/null @@ -1,10 +0,0 @@ -[discrete] -[[esql-is_nan]] -=== `IS_NAN` -Returns a boolean that indicates whether its input is not a number. - -[source,esql] ----- -ROW d = 1.0 -| EVAL s = IS_NAN(d) ----- diff --git a/x-pack/plugins/elastic_assistant/server/knowledge_base/esql/documentation/functions/least.asciidoc b/x-pack/plugins/elastic_assistant/server/knowledge_base/esql/documentation/functions/least.asciidoc deleted file mode 100644 index 62d7406199cd4..0000000000000 --- a/x-pack/plugins/elastic_assistant/server/knowledge_base/esql/documentation/functions/least.asciidoc +++ /dev/null @@ -1,25 +0,0 @@ -[discrete] -[[esql-least]] -=== `LEAST` -[.text-center] -image::esql/functions/signature/least.svg[Embedded,opts=inline] - -Returns the minimum value from many columns. This is similar to <> -except it's intended to run on multiple columns at once. - -[source.merge.styled,esql] ----- -include::{esql-specs}/math.csv-spec[tag=least] ----- -[%header.monospaced.styled,format=dsv,separator=|] -|=== -include::{esql-specs}/math.csv-spec[tag=least-result] -|=== - -NOTE: When run on `keyword` or `text` fields, this'll return the first string - in alphabetical order. When run on `boolean` columns this will return - `false` if any values are `false`. - -Supported types: - -include::types/least.asciidoc[] diff --git a/x-pack/plugins/elastic_assistant/server/knowledge_base/esql/documentation/functions/left.asciidoc b/x-pack/plugins/elastic_assistant/server/knowledge_base/esql/documentation/functions/left.asciidoc deleted file mode 100644 index 67e739377aa46..0000000000000 --- a/x-pack/plugins/elastic_assistant/server/knowledge_base/esql/documentation/functions/left.asciidoc +++ /dev/null @@ -1,20 +0,0 @@ -[discrete] -[[esql-left]] -=== `LEFT` -[.text-center] -image::esql/functions/signature/left.svg[Embedded,opts=inline] - -Return the substring that extracts 'length' chars from the 'string' starting from the left. - -[source.merge.styled,esql] ----- -include::{esql-specs}/string.csv-spec[tag=left] ----- -[%header.monospaced.styled,format=dsv,separator=|] -|=== -include::{esql-specs}/string.csv-spec[tag=left-result] -|=== - -Supported types: - -include::types/left.asciidoc[] diff --git a/x-pack/plugins/elastic_assistant/server/knowledge_base/esql/documentation/functions/length.asciidoc b/x-pack/plugins/elastic_assistant/server/knowledge_base/esql/documentation/functions/length.asciidoc deleted file mode 100644 index 12e1bed3d0a66..0000000000000 --- a/x-pack/plugins/elastic_assistant/server/knowledge_base/esql/documentation/functions/length.asciidoc +++ /dev/null @@ -1,11 +0,0 @@ -[discrete] -[[esql-length]] -=== `LENGTH` -Returns the character length of a string. - -[source,esql] ----- -FROM employees -| KEEP first_name, last_name, height -| EVAL fn_length = LENGTH(first_name) ----- diff --git a/x-pack/plugins/elastic_assistant/server/knowledge_base/esql/documentation/functions/like.asciidoc b/x-pack/plugins/elastic_assistant/server/knowledge_base/esql/documentation/functions/like.asciidoc deleted file mode 100644 index 9d06a3d051b93..0000000000000 --- a/x-pack/plugins/elastic_assistant/server/knowledge_base/esql/documentation/functions/like.asciidoc +++ /dev/null @@ -1,20 +0,0 @@ -[discrete] -[[esql-like-operator]] -=== `LIKE` - -Use `LIKE` to filter data based on string patterns using wildcards. `LIKE` -usually acts on a field placed on the left-hand side of the operator, but it can -also act on a constant (literal) expression. The right-hand side of the operator -represents the pattern. - -The following wildcard characters are supported: - -* `*` matches zero or more characters. -* `?` matches one character. - -[source,esql] ----- -FROM employees -| WHERE first_name LIKE "?b*" -| KEEP first_name, last_name ----- diff --git a/x-pack/plugins/elastic_assistant/server/knowledge_base/esql/documentation/functions/log10.asciidoc b/x-pack/plugins/elastic_assistant/server/knowledge_base/esql/documentation/functions/log10.asciidoc deleted file mode 100644 index 219519ca2a0d7..0000000000000 --- a/x-pack/plugins/elastic_assistant/server/knowledge_base/esql/documentation/functions/log10.asciidoc +++ /dev/null @@ -1,23 +0,0 @@ -[discrete] -[[esql-log10]] -=== `LOG10` -[.text-center] -image::esql/functions/signature/log10.svg[Embedded,opts=inline] - -Returns the log base 10. The input can be any numeric value, the return value -is always a double. - -Logs of negative numbers are NaN. Logs of infinites are infinite, as is the log of 0. - -[source.merge.styled,esql] ----- -include::{esql-specs}/math.csv-spec[tag=log10] ----- -[%header.monospaced.styled,format=dsv,separator=|] -|=== -include::{esql-specs}/math.csv-spec[tag=log10-result] -|=== - -Supported types: - -include::types/log10.asciidoc[] diff --git a/x-pack/plugins/elastic_assistant/server/knowledge_base/esql/documentation/functions/logical.asciidoc b/x-pack/plugins/elastic_assistant/server/knowledge_base/esql/documentation/functions/logical.asciidoc deleted file mode 100644 index 674ad67f99cde..0000000000000 --- a/x-pack/plugins/elastic_assistant/server/knowledge_base/esql/documentation/functions/logical.asciidoc +++ /dev/null @@ -1,9 +0,0 @@ -[discrete] -[[esql-logical-operators]] -=== Logical operators - -The following logical operators are supported: - -* `AND` -* `OR` -* `NOT` diff --git a/x-pack/plugins/elastic_assistant/server/knowledge_base/esql/documentation/functions/ltrim.asciidoc b/x-pack/plugins/elastic_assistant/server/knowledge_base/esql/documentation/functions/ltrim.asciidoc deleted file mode 100644 index 6e6d30a73b865..0000000000000 --- a/x-pack/plugins/elastic_assistant/server/knowledge_base/esql/documentation/functions/ltrim.asciidoc +++ /dev/null @@ -1,13 +0,0 @@ -[discrete] -[[esql-ltrim]] -=== `LTRIM` -Removes leading whitespaces from strings. - -[source.merge.styled,esql] ----- -include::{esql-specs}/string.csv-spec[tag=ltrim] ----- -[%header.monospaced.styled,format=dsv,separator=|] -|=== -include::{esql-specs}/string.csv-spec[tag=ltrim-result] -|=== diff --git a/x-pack/plugins/elastic_assistant/server/knowledge_base/esql/documentation/functions/math_functions.asciidoc b/x-pack/plugins/elastic_assistant/server/knowledge_base/esql/documentation/functions/math_functions.asciidoc deleted file mode 100644 index 21131ae9074d7..0000000000000 --- a/x-pack/plugins/elastic_assistant/server/knowledge_base/esql/documentation/functions/math_functions.asciidoc +++ /dev/null @@ -1,52 +0,0 @@ -[[esql-math-functions]] -==== {esql} mathematical functions - -++++ -Mathematical functions -++++ - -{esql} supports these mathematical functions: - -// tag::math_list[] -* <> -* <> -* <> -* <> -* <> -* <> -* <> -* <> -* <> -* <> -* <> -* <> -* <> -* <> -* <> -* <> -* <> -* <> -* <> -* <> -// end::math_list[] - -include::abs.asciidoc[] -include::acos.asciidoc[] -include::asin.asciidoc[] -include::atan.asciidoc[] -include::atan2.asciidoc[] -include::ceil.asciidoc[] -include::cos.asciidoc[] -include::cosh.asciidoc[] -include::e.asciidoc[] -include::floor.asciidoc[] -include::log10.asciidoc[] -include::pi.asciidoc[] -include::pow.asciidoc[] -include::round.asciidoc[] -include::sin.asciidoc[] -include::sinh.asciidoc[] -include::sqrt.asciidoc[] -include::tan.asciidoc[] -include::tanh.asciidoc[] -include::tau.asciidoc[] diff --git a/x-pack/plugins/elastic_assistant/server/knowledge_base/esql/documentation/functions/max.asciidoc b/x-pack/plugins/elastic_assistant/server/knowledge_base/esql/documentation/functions/max.asciidoc deleted file mode 100644 index 53997e501b37f..0000000000000 --- a/x-pack/plugins/elastic_assistant/server/knowledge_base/esql/documentation/functions/max.asciidoc +++ /dev/null @@ -1,13 +0,0 @@ -[discrete] -[[esql-agg-max]] -=== `MAX` -The maximum value of a numeric field. - -[source.merge.styled,esql] ----- -include::{esql-specs}/stats.csv-spec[tag=max] ----- -[%header.monospaced.styled,format=dsv,separator=|] -|=== -include::{esql-specs}/stats.csv-spec[tag=max-result] -|=== diff --git a/x-pack/plugins/elastic_assistant/server/knowledge_base/esql/documentation/functions/median.asciidoc b/x-pack/plugins/elastic_assistant/server/knowledge_base/esql/documentation/functions/median.asciidoc deleted file mode 100644 index 5a0d0c049602e..0000000000000 --- a/x-pack/plugins/elastic_assistant/server/knowledge_base/esql/documentation/functions/median.asciidoc +++ /dev/null @@ -1,22 +0,0 @@ -[discrete] -[[esql-agg-median]] -=== `MEDIAN` -The value that is greater than half of all values and less than half of -all values, also known as the 50% <>. - -[source.merge.styled,esql] ----- -include::{esql-specs}/stats_percentile.csv-spec[tag=median] ----- -[%header.monospaced.styled,format=dsv,separator=|] -|=== -include::{esql-specs}/stats_percentile.csv-spec[tag=median-result] -|=== - -NOTE: Like <>, `MEDIAN` is <>. - -[WARNING] -==== -`MEDIAN` is also {wikipedia}/Nondeterministic_algorithm[non-deterministic]. -This means you can get slightly different results using the same data. -==== diff --git a/x-pack/plugins/elastic_assistant/server/knowledge_base/esql/documentation/functions/median_absolute_deviation.asciidoc b/x-pack/plugins/elastic_assistant/server/knowledge_base/esql/documentation/functions/median_absolute_deviation.asciidoc deleted file mode 100644 index fe0923da1fb88..0000000000000 --- a/x-pack/plugins/elastic_assistant/server/knowledge_base/esql/documentation/functions/median_absolute_deviation.asciidoc +++ /dev/null @@ -1,29 +0,0 @@ -[discrete] -[[esql-agg-median-absolute-deviation]] -=== `MEDIAN_ABSOLUTE_DEVIATION` -The median absolute deviation, a measure of variability. It is a robust -statistic, meaning that it is useful for describing data that may have outliers, -or may not be normally distributed. For such data it can be more descriptive than -standard deviation. - -It is calculated as the median of each data point’s deviation from the median of -the entire sample. That is, for a random variable `X`, the median absolute deviation -is `median(|median(X) - Xi|)`. - -[source.merge.styled,esql] ----- -include::{esql-specs}/stats_percentile.csv-spec[tag=median-absolute-deviation] ----- -[%header.monospaced.styled,format=dsv,separator=|] -|=== -include::{esql-specs}/stats_percentile.csv-spec[tag=median-absolute-deviation-result] -|=== - -NOTE: Like <>, `MEDIAN_ABSOLUTE_DEVIATION` is - <>. - -[WARNING] -==== -`MEDIAN_ABSOLUTE_DEVIATION` is also {wikipedia}/Nondeterministic_algorithm[non-deterministic]. -This means you can get slightly different results using the same data. -==== diff --git a/x-pack/plugins/elastic_assistant/server/knowledge_base/esql/documentation/functions/min.asciidoc b/x-pack/plugins/elastic_assistant/server/knowledge_base/esql/documentation/functions/min.asciidoc deleted file mode 100644 index a143cca69c01a..0000000000000 --- a/x-pack/plugins/elastic_assistant/server/knowledge_base/esql/documentation/functions/min.asciidoc +++ /dev/null @@ -1,13 +0,0 @@ -[discrete] -[[esql-agg-min]] -=== `MIN` -The minimum value of a numeric field. - -[source.merge.styled,esql] ----- -include::{esql-specs}/stats.csv-spec[tag=min] ----- -[%header.monospaced.styled,format=dsv,separator=|] -|=== -include::{esql-specs}/stats.csv-spec[tag=min-result] -|=== diff --git a/x-pack/plugins/elastic_assistant/server/knowledge_base/esql/documentation/functions/mv_avg.asciidoc b/x-pack/plugins/elastic_assistant/server/knowledge_base/esql/documentation/functions/mv_avg.asciidoc deleted file mode 100644 index ad5f672205516..0000000000000 --- a/x-pack/plugins/elastic_assistant/server/knowledge_base/esql/documentation/functions/mv_avg.asciidoc +++ /dev/null @@ -1,17 +0,0 @@ -[discrete] -[[esql-mv_avg]] -=== `MV_AVG` -Converts a multivalued field into a single valued field containing the average -of all of the values. For example: - -[source.merge.styled,esql] ----- -include::{esql-specs}/math.csv-spec[tag=mv_avg] ----- -[%header.monospaced.styled,format=dsv,separator=|] -|=== -include::{esql-specs}/math.csv-spec[tag=mv_avg-result] -|=== - - -NOTE: The output type is always a `double` and the input type can be any number. diff --git a/x-pack/plugins/elastic_assistant/server/knowledge_base/esql/documentation/functions/mv_concat.asciidoc b/x-pack/plugins/elastic_assistant/server/knowledge_base/esql/documentation/functions/mv_concat.asciidoc deleted file mode 100644 index d4be458455131..0000000000000 --- a/x-pack/plugins/elastic_assistant/server/knowledge_base/esql/documentation/functions/mv_concat.asciidoc +++ /dev/null @@ -1,26 +0,0 @@ -[discrete] -[[esql-mv_concat]] -=== `MV_CONCAT` -Converts a multivalued string field into a single valued field containing the -concatenation of all values separated by a delimiter: - -[source.merge.styled,esql] ----- -include::{esql-specs}/string.csv-spec[tag=mv_concat] ----- -[%header.monospaced.styled,format=dsv,separator=|] -|=== -include::{esql-specs}/string.csv-spec[tag=mv_concat-result] -|=== - -If you want to concat non-string fields call <> on them first: - -[source.merge.styled,esql] ----- -include::{esql-specs}/string.csv-spec[tag=mv_concat-to_string] ----- -[%header.monospaced.styled,format=dsv,separator=|] -|=== -include::{esql-specs}/string.csv-spec[tag=mv_concat-to_string-result] -|=== - diff --git a/x-pack/plugins/elastic_assistant/server/knowledge_base/esql/documentation/functions/mv_count.asciidoc b/x-pack/plugins/elastic_assistant/server/knowledge_base/esql/documentation/functions/mv_count.asciidoc deleted file mode 100644 index 5bcda53ca5a9b..0000000000000 --- a/x-pack/plugins/elastic_assistant/server/knowledge_base/esql/documentation/functions/mv_count.asciidoc +++ /dev/null @@ -1,16 +0,0 @@ -[discrete] -[[esql-mv_count]] -=== `MV_COUNT` -Converts a multivalued field into a single valued field containing a count of the number -of values: - -[source.merge.styled,esql] ----- -include::{esql-specs}/string.csv-spec[tag=mv_count] ----- -[%header.monospaced.styled,format=dsv,separator=|] -|=== -include::{esql-specs}/string.csv-spec[tag=mv_count-result] -|=== - -NOTE: This function accepts all types and always returns an `integer`. diff --git a/x-pack/plugins/elastic_assistant/server/knowledge_base/esql/documentation/functions/mv_dedupe.asciidoc b/x-pack/plugins/elastic_assistant/server/knowledge_base/esql/documentation/functions/mv_dedupe.asciidoc deleted file mode 100644 index c6af3f2d1aa3f..0000000000000 --- a/x-pack/plugins/elastic_assistant/server/knowledge_base/esql/documentation/functions/mv_dedupe.asciidoc +++ /dev/null @@ -1,15 +0,0 @@ -[discrete] -[[esql-mv_dedupe]] -=== `MV_DEDUPE` -Removes duplicates from a multivalued field. For example: - -[source.merge.styled,esql] ----- -include::{esql-specs}/string.csv-spec[tag=mv_dedupe] ----- -[%header.monospaced.styled,format=dsv,separator=|] -|=== -include::{esql-specs}/string.csv-spec[tag=mv_dedupe-result] -|=== - -NOTE: `MV_DEDUPE` may, but won't always, sort the values in the field. diff --git a/x-pack/plugins/elastic_assistant/server/knowledge_base/esql/documentation/functions/mv_functions.asciidoc b/x-pack/plugins/elastic_assistant/server/knowledge_base/esql/documentation/functions/mv_functions.asciidoc deleted file mode 100644 index 83dbaaadc5c06..0000000000000 --- a/x-pack/plugins/elastic_assistant/server/knowledge_base/esql/documentation/functions/mv_functions.asciidoc +++ /dev/null @@ -1,28 +0,0 @@ -[[esql-mv-functions]] -==== {esql} multivalue functions - -++++ -Multivalue functions -++++ - -{esql} supports these multivalue functions: - -// tag::mv_list[] -* <> -* <> -* <> -* <> -* <> -* <> -* <> -* <> -// end::mv_list[] - -include::mv_avg.asciidoc[] -include::mv_concat.asciidoc[] -include::mv_count.asciidoc[] -include::mv_dedupe.asciidoc[] -include::mv_max.asciidoc[] -include::mv_median.asciidoc[] -include::mv_min.asciidoc[] -include::mv_sum.asciidoc[] diff --git a/x-pack/plugins/elastic_assistant/server/knowledge_base/esql/documentation/functions/mv_max.asciidoc b/x-pack/plugins/elastic_assistant/server/knowledge_base/esql/documentation/functions/mv_max.asciidoc deleted file mode 100644 index e8ef951f168f5..0000000000000 --- a/x-pack/plugins/elastic_assistant/server/knowledge_base/esql/documentation/functions/mv_max.asciidoc +++ /dev/null @@ -1,25 +0,0 @@ -[discrete] -[[esql-mv_max]] -=== `MV_MAX` -Converts a multivalued field into a single valued field containing the maximum value. For example: - -[source.merge.styled,esql] ----- -include::{esql-specs}/math.csv-spec[tag=mv_max] ----- -[%header.monospaced.styled,format=dsv,separator=|] -|=== -include::{esql-specs}/math.csv-spec[tag=mv_max-result] -|=== - -It can be used by any field type, including `keyword` fields. In that case picks the -last string, comparing their utf-8 representation byte by byte: - -[source.merge.styled,esql] ----- -include::{esql-specs}/string.csv-spec[tag=mv_max] ----- -[%header.monospaced.styled,format=dsv,separator=|] -|=== -include::{esql-specs}/string.csv-spec[tag=mv_max-result] -|=== diff --git a/x-pack/plugins/elastic_assistant/server/knowledge_base/esql/documentation/functions/mv_median.asciidoc b/x-pack/plugins/elastic_assistant/server/knowledge_base/esql/documentation/functions/mv_median.asciidoc deleted file mode 100644 index c84cf7a895da5..0000000000000 --- a/x-pack/plugins/elastic_assistant/server/knowledge_base/esql/documentation/functions/mv_median.asciidoc +++ /dev/null @@ -1,27 +0,0 @@ -[discrete] -[[esql-mv_median]] -=== `MV_MEDIAN` -Converts a multivalued field into a single valued field containing the median value. For example: - -[source.merge.styled,esql] ----- -include::{esql-specs}/math.csv-spec[tag=mv_median] ----- -[%header.monospaced.styled,format=dsv,separator=|] -|=== -include::{esql-specs}/math.csv-spec[tag=mv_median-result] -|=== - -It can be used by any numeric field type and returns a value of the same type. If the -row has an even number of values for a column the result will be the average of the -middle two entries. If the field is not floating point then the average rounds *down*: - -[source.merge.styled,esql] ----- -include::{esql-specs}/math.csv-spec[tag=mv_median_round_down] ----- -[%header.monospaced.styled,format=dsv,separator=|] -|=== -include::{esql-specs}/math.csv-spec[tag=mv_median_round_down-result] -|=== - diff --git a/x-pack/plugins/elastic_assistant/server/knowledge_base/esql/documentation/functions/mv_min.asciidoc b/x-pack/plugins/elastic_assistant/server/knowledge_base/esql/documentation/functions/mv_min.asciidoc deleted file mode 100644 index 235e5c3c2bb5e..0000000000000 --- a/x-pack/plugins/elastic_assistant/server/knowledge_base/esql/documentation/functions/mv_min.asciidoc +++ /dev/null @@ -1,25 +0,0 @@ -[discrete] -[[esql-mv_min]] -=== `MV_MIN` -Converts a multivalued field into a single valued field containing the minimum value. For example: - -[source.merge.styled,esql] ----- -include::{esql-specs}/math.csv-spec[tag=mv_min] ----- -[%header.monospaced.styled,format=dsv,separator=|] -|=== -include::{esql-specs}/math.csv-spec[tag=mv_min-result] -|=== - -It can be used by any field type, including `keyword` fields. In that case picks the -first string, comparing their utf-8 representation byte by byte: - -[source.merge.styled,esql] ----- -include::{esql-specs}/string.csv-spec[tag=mv_min] ----- -[%header.monospaced.styled,format=dsv,separator=|] -|=== -include::{esql-specs}/string.csv-spec[tag=mv_min-result] -|=== diff --git a/x-pack/plugins/elastic_assistant/server/knowledge_base/esql/documentation/functions/mv_sum.asciidoc b/x-pack/plugins/elastic_assistant/server/knowledge_base/esql/documentation/functions/mv_sum.asciidoc deleted file mode 100644 index 646af03305954..0000000000000 --- a/x-pack/plugins/elastic_assistant/server/knowledge_base/esql/documentation/functions/mv_sum.asciidoc +++ /dev/null @@ -1,16 +0,0 @@ -[discrete] -[[esql-mv_sum]] -=== `MV_SUM` -Converts a multivalued field into a single valued field containing the sum -of all of the values. For example: - -[source.merge.styled,esql] ----- -include::{esql-specs}/math.csv-spec[tag=mv_sum] ----- -[%header.monospaced.styled,format=dsv,separator=|] -|=== -include::{esql-specs}/math.csv-spec[tag=mv_sum-result] -|=== - -NOTE: The input type can be any number and the output type is the same as the input type. diff --git a/x-pack/plugins/elastic_assistant/server/knowledge_base/esql/documentation/functions/now.asciidoc b/x-pack/plugins/elastic_assistant/server/knowledge_base/esql/documentation/functions/now.asciidoc deleted file mode 100644 index 5d33449a1e906..0000000000000 --- a/x-pack/plugins/elastic_assistant/server/knowledge_base/esql/documentation/functions/now.asciidoc +++ /dev/null @@ -1,9 +0,0 @@ -[discrete] -[[esql-now]] -=== `NOW` -Returns current date and time. - -[source,esql] ----- -ROW current_date = NOW() ----- diff --git a/x-pack/plugins/elastic_assistant/server/knowledge_base/esql/documentation/functions/operators.asciidoc b/x-pack/plugins/elastic_assistant/server/knowledge_base/esql/documentation/functions/operators.asciidoc deleted file mode 100644 index c236413b5dd7e..0000000000000 --- a/x-pack/plugins/elastic_assistant/server/knowledge_base/esql/documentation/functions/operators.asciidoc +++ /dev/null @@ -1,36 +0,0 @@ -[[esql-operators]] -==== {esql} operators - -++++ -Operators -++++ - -Boolean operators for comparing against one or multiple expressions. - -// tag::op_list[] -* <> -* <> -* <> -* <> -* <> -* <> -* <> -* <> -* <> -* <> -* <> -* <> -// end::op_list[] - -include::binary.asciidoc[] -include::logical.asciidoc[] -include::predicates.asciidoc[] -include::cidr_match.asciidoc[] -include::ends_with.asciidoc[] -include::in.asciidoc[] -include::is_finite.asciidoc[] -include::is_infinite.asciidoc[] -include::is_nan.asciidoc[] -include::like.asciidoc[] -include::rlike.asciidoc[] -include::starts_with.asciidoc[] diff --git a/x-pack/plugins/elastic_assistant/server/knowledge_base/esql/documentation/functions/percentile.asciidoc b/x-pack/plugins/elastic_assistant/server/knowledge_base/esql/documentation/functions/percentile.asciidoc deleted file mode 100644 index 917a4a81e7b4f..0000000000000 --- a/x-pack/plugins/elastic_assistant/server/knowledge_base/esql/documentation/functions/percentile.asciidoc +++ /dev/null @@ -1,30 +0,0 @@ -[discrete] -[[esql-agg-percentile]] -=== `PERCENTILE` -The value at which a certain percentage of observed values occur. For example, -the 95th percentile is the value which is greater than 95% of the observed values and -the 50th percentile is the <>. - -[source.merge.styled,esql] ----- -include::{esql-specs}/stats_percentile.csv-spec[tag=percentile] ----- -[%header.monospaced.styled,format=dsv,separator=|] -|=== -include::{esql-specs}/stats_percentile.csv-spec[tag=percentile-result] -|=== - -[discrete] -[[esql-agg-percentile-approximate]] -==== `PERCENTILE` is (usually) approximate - -include::../../aggregations/metrics/percentile-aggregation.asciidoc[tag=approximate] - -[WARNING] -==== -`PERCENTILE` is also {wikipedia}/Nondeterministic_algorithm[non-deterministic]. -This means you can get slightly different results using the same data. -==== - - - diff --git a/x-pack/plugins/elastic_assistant/server/knowledge_base/esql/documentation/functions/pi.asciidoc b/x-pack/plugins/elastic_assistant/server/knowledge_base/esql/documentation/functions/pi.asciidoc deleted file mode 100644 index cd630aaabadcd..0000000000000 --- a/x-pack/plugins/elastic_assistant/server/knowledge_base/esql/documentation/functions/pi.asciidoc +++ /dev/null @@ -1,16 +0,0 @@ -[discrete] -[[esql-pi]] -=== `PI` -[.text-center] -image::esql/functions/signature/pi.svg[Embedded,opts=inline] - -The {wikipedia}/Pi[ratio] of a circle's circumference to its diameter. - -[source.merge.styled,esql] ----- -include::{esql-specs}/math.csv-spec[tag=pi] ----- -[%header.monospaced.styled,format=dsv,separator=|] -|=== -include::{esql-specs}/math.csv-spec[tag=pi-result] -|=== diff --git a/x-pack/plugins/elastic_assistant/server/knowledge_base/esql/documentation/functions/pow.asciidoc b/x-pack/plugins/elastic_assistant/server/knowledge_base/esql/documentation/functions/pow.asciidoc deleted file mode 100644 index 9f7805bfd3eae..0000000000000 --- a/x-pack/plugins/elastic_assistant/server/knowledge_base/esql/documentation/functions/pow.asciidoc +++ /dev/null @@ -1,96 +0,0 @@ -[discrete] -[[esql-pow]] -=== `POW` -[.text-center] -image::esql/functions/signature/pow.svg[Embedded,opts=inline] - -Returns the value of a base (first argument) raised to the power of an exponent (second argument). -Both arguments must be numeric. - -[source.merge.styled,esql] ----- -include::{esql-specs}/math.csv-spec[tag=powDI] ----- -[%header.monospaced.styled,format=dsv,separator=|] -|=== -include::{esql-specs}/math.csv-spec[tag=powDI-result] -|=== - -[discrete] -==== Type rules - -The type of the returned value is determined by the types of the base and exponent. -The following rules are applied to determine the result type: - -* If either of the base or exponent are of a floating point type, the result will be a double -* Otherwise, if either the base or the exponent are 64-bit (long or unsigned long), the result will be a long -* Otherwise, the result will be a 32-bit integer (this covers all other numeric types, including int, short and byte) - -For example, using simple integers as arguments will lead to an integer result: - -[source.merge.styled,esql] ----- -include::{esql-specs}/math.csv-spec[tag=powII] ----- -[%header.monospaced.styled,format=dsv,separator=|] -|=== -include::{esql-specs}/math.csv-spec[tag=powII-result] -|=== - -NOTE: The actual power function is performed using double precision values for all cases. -This means that for very large non-floating point values there is a small chance that the -operation can lead to slightly different answers than expected. -However, a more likely outcome of very large non-floating point values is numerical overflow. - -[discrete] -==== Arithmetic errors - -Arithmetic errors and numeric overflow do not result in an error. Instead, the result will be `null` -and a warning for the `ArithmeticException` added. -For example: - -[source.merge.styled,esql] ----- -include::{esql-specs}/math.csv-spec[tag=powULOverrun] ----- -[%header.monospaced.styled,format=dsv,separator=|] -|=== -include::{esql-specs}/math.csv-spec[tag=powULOverrun-warning] -|=== -[%header.monospaced.styled,format=dsv,separator=|] -|=== -include::{esql-specs}/math.csv-spec[tag=powULOverrun-result] -|=== - -If it is desired to protect against numerical overruns, use `TO_DOUBLE` on either of the arguments: - -[source.merge.styled,esql] ----- -include::{esql-specs}/math.csv-spec[tag=pow2d] ----- -[%header.monospaced.styled,format=dsv,separator=|] -|=== -include::{esql-specs}/math.csv-spec[tag=pow2d-result] -|=== - -[discrete] -==== Fractional exponents - -The exponent can be a fraction, which is similar to performing a root. -For example, the exponent of `0.5` will give the square root of the base: - -[source.merge.styled,esql] ----- -include::{esql-specs}/math.csv-spec[tag=powID-sqrt] ----- -[%header.monospaced.styled,format=dsv,separator=|] -|=== -include::{esql-specs}/math.csv-spec[tag=powID-sqrt-result] -|=== - -[discrete] -==== Table of supported input and output types - -For clarity, the following table describes the output result type for all combinations of numeric input types: - -include::types/pow.asciidoc[] diff --git a/x-pack/plugins/elastic_assistant/server/knowledge_base/esql/documentation/functions/predicates.asciidoc b/x-pack/plugins/elastic_assistant/server/knowledge_base/esql/documentation/functions/predicates.asciidoc deleted file mode 100644 index 9a3ea89e9aa73..0000000000000 --- a/x-pack/plugins/elastic_assistant/server/knowledge_base/esql/documentation/functions/predicates.asciidoc +++ /dev/null @@ -1,23 +0,0 @@ -[discrete] -[[esql-predicates]] -=== `IS NULL` and `IS NOT NULL` predicates - -For NULL comparison, use the `IS NULL` and `IS NOT NULL` predicates: - -[source.merge.styled,esql] ----- -include::{esql-specs}/null.csv-spec[tag=is-null] ----- -[%header.monospaced.styled,format=dsv,separator=|] -|=== -include::{esql-specs}/null.csv-spec[tag=is-null-result] -|=== - -[source.merge.styled,esql] ----- -include::{esql-specs}/null.csv-spec[tag=is-not-null] ----- -[%header.monospaced.styled,format=dsv,separator=|] -|=== -include::{esql-specs}/null.csv-spec[tag=is-not-null-result] -|=== diff --git a/x-pack/plugins/elastic_assistant/server/knowledge_base/esql/documentation/functions/replace.asciidoc b/x-pack/plugins/elastic_assistant/server/knowledge_base/esql/documentation/functions/replace.asciidoc deleted file mode 100644 index 9bc0f85fdddce..0000000000000 --- a/x-pack/plugins/elastic_assistant/server/knowledge_base/esql/documentation/functions/replace.asciidoc +++ /dev/null @@ -1,17 +0,0 @@ -[discrete] -[[esql-replace]] -=== `REPLACE` -The function substitutes in the string (1st argument) any match of the regular expression (2nd argument) with the replacement string (3rd argument). - -If any of the arguments are `NULL`, the result is `NULL`. - -. This example replaces an occurrence of the word "World" with the word "Universe": - -[source.merge.styled,esql] ----- -include::{esql-specs}/docs.csv-spec[tag=replaceString] ----- -[%header.monospaced.styled,format=dsv,separator=|] -|=== -include::{esql-specs}/docs.csv-spec[tag=replaceString-result] -|=== diff --git a/x-pack/plugins/elastic_assistant/server/knowledge_base/esql/documentation/functions/right.asciidoc b/x-pack/plugins/elastic_assistant/server/knowledge_base/esql/documentation/functions/right.asciidoc deleted file mode 100644 index a0f18192d410d..0000000000000 --- a/x-pack/plugins/elastic_assistant/server/knowledge_base/esql/documentation/functions/right.asciidoc +++ /dev/null @@ -1,20 +0,0 @@ -[discrete] -[[esql-right]] -=== `RIGHT` -[.text-center] -image::esql/functions/signature/right.svg[Embedded,opts=inline] - -Return the substring that extracts 'length' chars from the 'string' starting from the right. - -[source.merge.styled,esql] ----- -include::{esql-specs}/string.csv-spec[tag=right] ----- -[%header.monospaced.styled,format=dsv,separator=|] -|=== -include::{esql-specs}/string.csv-spec[tag=right-result] -|=== - -Supported types: - -include::types/right.asciidoc[] diff --git a/x-pack/plugins/elastic_assistant/server/knowledge_base/esql/documentation/functions/rlike.asciidoc b/x-pack/plugins/elastic_assistant/server/knowledge_base/esql/documentation/functions/rlike.asciidoc deleted file mode 100644 index 0fd8d8ab319da..0000000000000 --- a/x-pack/plugins/elastic_assistant/server/knowledge_base/esql/documentation/functions/rlike.asciidoc +++ /dev/null @@ -1,15 +0,0 @@ -[discete] -[[esql-rlike-operator]] -==== `RLIKE` - -Use `RLIKE` to filter data based on string patterns using using -<>. `RLIKE` usually acts on a field placed on -the left-hand side of the operator, but it can also act on a constant (literal) -expression. The right-hand side of the operator represents the pattern. - -[source,esql] ----- -FROM employees -| WHERE first_name RLIKE ".leja.*" -| KEEP first_name, last_name ----- \ No newline at end of file diff --git a/x-pack/plugins/elastic_assistant/server/knowledge_base/esql/documentation/functions/round.asciidoc b/x-pack/plugins/elastic_assistant/server/knowledge_base/esql/documentation/functions/round.asciidoc deleted file mode 100644 index 4ec71cf682d0f..0000000000000 --- a/x-pack/plugins/elastic_assistant/server/knowledge_base/esql/documentation/functions/round.asciidoc +++ /dev/null @@ -1,15 +0,0 @@ -[discrete] -[[esql-round]] -=== `ROUND` -Rounds a number to the closest number with the specified number of digits. -Defaults to 0 digits if no number of digits is provided. If the specified number -of digits is negative, rounds to the number of digits left of the decimal point. - -[source.merge.styled,esql] ----- -include::{esql-specs}/docs.csv-spec[tag=round] ----- -[%header.monospaced.styled,format=dsv,separator=|] -|=== -include::{esql-specs}/docs.csv-spec[tag=round-result] -|=== diff --git a/x-pack/plugins/elastic_assistant/server/knowledge_base/esql/documentation/functions/rtrim.asciidoc b/x-pack/plugins/elastic_assistant/server/knowledge_base/esql/documentation/functions/rtrim.asciidoc deleted file mode 100644 index 3224331e9ed6a..0000000000000 --- a/x-pack/plugins/elastic_assistant/server/knowledge_base/esql/documentation/functions/rtrim.asciidoc +++ /dev/null @@ -1,13 +0,0 @@ -[discrete] -[[esql-rtrim]] -=== `RTRIM` -Removes trailing whitespaces from strings. - -[source.merge.styled,esql] ----- -include::{esql-specs}/string.csv-spec[tag=rtrim] ----- -[%header.monospaced.styled,format=dsv,separator=|] -|=== -include::{esql-specs}/string.csv-spec[tag=rtrim-result] -|=== diff --git a/x-pack/plugins/elastic_assistant/server/knowledge_base/esql/documentation/functions/sin.asciidoc b/x-pack/plugins/elastic_assistant/server/knowledge_base/esql/documentation/functions/sin.asciidoc deleted file mode 100644 index 5fa33a315392d..0000000000000 --- a/x-pack/plugins/elastic_assistant/server/knowledge_base/esql/documentation/functions/sin.asciidoc +++ /dev/null @@ -1,20 +0,0 @@ -[discrete] -[[esql-sin]] -=== `SIN` -[.text-center] -image::esql/functions/signature/sin.svg[Embedded,opts=inline] - -https://en.wikipedia.org/wiki/Sine_and_cosine[Sine] trigonometric function. - -[source.merge.styled,esql] ----- -include::{esql-specs}/floats.csv-spec[tag=sin] ----- -[%header.monospaced.styled,format=dsv,separator=|] -|=== -include::{esql-specs}/floats.csv-spec[tag=sin-result] -|=== - -Supported types: - -include::types/sin.asciidoc[] diff --git a/x-pack/plugins/elastic_assistant/server/knowledge_base/esql/documentation/functions/sinh.asciidoc b/x-pack/plugins/elastic_assistant/server/knowledge_base/esql/documentation/functions/sinh.asciidoc deleted file mode 100644 index 11d1ea29bffef..0000000000000 --- a/x-pack/plugins/elastic_assistant/server/knowledge_base/esql/documentation/functions/sinh.asciidoc +++ /dev/null @@ -1,20 +0,0 @@ -[discrete] -[[esql-sinh]] -=== `SINH` -[.text-center] -image::esql/functions/signature/sinh.svg[Embedded,opts=inline] - -https://en.wikipedia.org/wiki/Hyperbolic_functions[Sine] hyperbolic function. - -[source.merge.styled,esql] ----- -include::{esql-specs}/floats.csv-spec[tag=sinh] ----- -[%header.monospaced.styled,format=dsv,separator=|] -|=== -include::{esql-specs}/floats.csv-spec[tag=sinh-result] -|=== - -Supported types: - -include::types/sinh.asciidoc[] diff --git a/x-pack/plugins/elastic_assistant/server/knowledge_base/esql/documentation/functions/split.asciidoc b/x-pack/plugins/elastic_assistant/server/knowledge_base/esql/documentation/functions/split.asciidoc deleted file mode 100644 index a6f8869bf89ca..0000000000000 --- a/x-pack/plugins/elastic_assistant/server/knowledge_base/esql/documentation/functions/split.asciidoc +++ /dev/null @@ -1,18 +0,0 @@ -[discrete] -[[esql-split]] -=== `SPLIT` -Split a single valued string into multiple strings. For example: - -[source,esql] ----- -include::{esql-specs}/string.csv-spec[tag=split] ----- - -Which splits `"foo;bar;baz;qux;quux;corge"` on `;` and returns an array: - -[%header,format=dsv,separator=|] -|=== -include::{esql-specs}/string.csv-spec[tag=split-result] -|=== - -WARNING: Only single byte delimiters are currently supported. diff --git a/x-pack/plugins/elastic_assistant/server/knowledge_base/esql/documentation/functions/sqrt.asciidoc b/x-pack/plugins/elastic_assistant/server/knowledge_base/esql/documentation/functions/sqrt.asciidoc deleted file mode 100644 index 02f7060089971..0000000000000 --- a/x-pack/plugins/elastic_assistant/server/knowledge_base/esql/documentation/functions/sqrt.asciidoc +++ /dev/null @@ -1,23 +0,0 @@ -[discrete] -[[esql-sqrt]] -=== `SQRT` -[.text-center] -image::esql/functions/signature/sqrt.svg[Embedded,opts=inline] - -Returns the square root of a number. The input can be any numeric value, the return value -is always a double. - -Square roots of negative numbers are NaN. Square roots of infinites are infinite. - -[source.merge.styled,esql] ----- -include::{esql-specs}/math.csv-spec[tag=sqrt] ----- -[%header.monospaced.styled,format=dsv,separator=|] -|=== -include::{esql-specs}/math.csv-spec[tag=sqrt-result] -|=== - -Supported types: - -include::types/sqrt.asciidoc[] diff --git a/x-pack/plugins/elastic_assistant/server/knowledge_base/esql/documentation/functions/starts_with.asciidoc b/x-pack/plugins/elastic_assistant/server/knowledge_base/esql/documentation/functions/starts_with.asciidoc deleted file mode 100644 index 38cee79ea63f8..0000000000000 --- a/x-pack/plugins/elastic_assistant/server/knowledge_base/esql/documentation/functions/starts_with.asciidoc +++ /dev/null @@ -1,21 +0,0 @@ -[discrete] -[[esql-starts_with]] -=== `STARTS_WITH` -[.text-center] -image::esql/functions/signature/ends_with.svg[Embedded,opts=inline] - -Returns a boolean that indicates whether a keyword string starts with another -string: - -[source.merge.styled,esql] ----- -include::{esql-specs}/docs.csv-spec[tag=startsWith] ----- -[%header.monospaced.styled,format=dsv,separator=|] -|=== -include::{esql-specs}/docs.csv-spec[tag=startsWith-result] -|=== - -Supported types: - -include::types/starts_with.asciidoc[] diff --git a/x-pack/plugins/elastic_assistant/server/knowledge_base/esql/documentation/functions/string_functions.asciidoc b/x-pack/plugins/elastic_assistant/server/knowledge_base/esql/documentation/functions/string_functions.asciidoc deleted file mode 100644 index b209244b93297..0000000000000 --- a/x-pack/plugins/elastic_assistant/server/knowledge_base/esql/documentation/functions/string_functions.asciidoc +++ /dev/null @@ -1,32 +0,0 @@ -[[esql-string-functions]] -==== {esql} string functions - -++++ -String functions -++++ - -{esql} supports these string functions: - -// tag::string_list[] -* <> -* <> -* <> -* <> -* <> -* <> -* <> -* <> -* <> -* <> -// end::string_list[] - -include::concat.asciidoc[] -include::left.asciidoc[] -include::length.asciidoc[] -include::ltrim.asciidoc[] -include::replace.asciidoc[] -include::right.asciidoc[] -include::rtrim.asciidoc[] -include::split.asciidoc[] -include::substring.asciidoc[] -include::trim.asciidoc[] diff --git a/x-pack/plugins/elastic_assistant/server/knowledge_base/esql/documentation/functions/substring.asciidoc b/x-pack/plugins/elastic_assistant/server/knowledge_base/esql/documentation/functions/substring.asciidoc deleted file mode 100644 index 8b8234de05bba..0000000000000 --- a/x-pack/plugins/elastic_assistant/server/knowledge_base/esql/documentation/functions/substring.asciidoc +++ /dev/null @@ -1,38 +0,0 @@ -[discrete] -[[esql-substring]] -=== `SUBSTRING` -Returns a substring of a string, specified by a start position and an optional -length. This example returns the first three characters of every last name: - -[source.merge.styled,esql] ----- -include::{esql-specs}/docs.csv-spec[tag=substring] ----- -[%header.monospaced.styled,format=dsv,separator=|] -|=== -include::{esql-specs}/docs.csv-spec[tag=substring-result] -|=== - -A negative start position is interpreted as being relative to the end of the -string. This example returns the last three characters of of every last name: - -[source.merge.styled,esql] ----- -include::{esql-specs}/docs.csv-spec[tag=substringEnd] ----- -[%header.monospaced.styled,format=dsv,separator=|] -|=== -include::{esql-specs}/docs.csv-spec[tag=substringEnd-result] -|=== - -If length is omitted, substring returns the remainder of the string. This -example returns all characters except for the first: - -[source.merge.styled,esql] ----- -include::{esql-specs}/docs.csv-spec[tag=substringRemainder] ----- -[%header.monospaced.styled,format=dsv,separator=|] -|=== -include::{esql-specs}/docs.csv-spec[tag=substringRemainder-result] -|=== diff --git a/x-pack/plugins/elastic_assistant/server/knowledge_base/esql/documentation/functions/sum.asciidoc b/x-pack/plugins/elastic_assistant/server/knowledge_base/esql/documentation/functions/sum.asciidoc deleted file mode 100644 index abf790040114d..0000000000000 --- a/x-pack/plugins/elastic_assistant/server/knowledge_base/esql/documentation/functions/sum.asciidoc +++ /dev/null @@ -1,13 +0,0 @@ -[discrete] -[[esql-agg-sum]] -=== `SUM` -The sum of a numeric field. - -[source.merge.styled,esql] ----- -include::{esql-specs}/stats.csv-spec[tag=sum] ----- -[%header.monospaced.styled,format=dsv,separator=|] -|=== -include::{esql-specs}/stats.csv-spec[tag=sum-result] -|=== diff --git a/x-pack/plugins/elastic_assistant/server/knowledge_base/esql/documentation/functions/tan.asciidoc b/x-pack/plugins/elastic_assistant/server/knowledge_base/esql/documentation/functions/tan.asciidoc deleted file mode 100644 index 1c66562eada7a..0000000000000 --- a/x-pack/plugins/elastic_assistant/server/knowledge_base/esql/documentation/functions/tan.asciidoc +++ /dev/null @@ -1,20 +0,0 @@ -[discrete] -[[esql-tan]] -=== `TAN` -[.text-center] -image::esql/functions/signature/tan.svg[Embedded,opts=inline] - -https://en.wikipedia.org/wiki/Sine_and_cosine[Tangent] trigonometric function. - -[source.merge.styled,esql] ----- -include::{esql-specs}/floats.csv-spec[tag=tan] ----- -[%header.monospaced.styled,format=dsv,separator=|] -|=== -include::{esql-specs}/floats.csv-spec[tag=tan-result] -|=== - -Supported types: - -include::types/tan.asciidoc[] diff --git a/x-pack/plugins/elastic_assistant/server/knowledge_base/esql/documentation/functions/tanh.asciidoc b/x-pack/plugins/elastic_assistant/server/knowledge_base/esql/documentation/functions/tanh.asciidoc deleted file mode 100644 index 218a0155d861c..0000000000000 --- a/x-pack/plugins/elastic_assistant/server/knowledge_base/esql/documentation/functions/tanh.asciidoc +++ /dev/null @@ -1,20 +0,0 @@ -[discrete] -[[esql-tanh]] -=== `TANH` -[.text-center] -image::esql/functions/signature/tanh.svg[Embedded,opts=inline] - -https://en.wikipedia.org/wiki/Hyperbolic_functions[Tangent] hyperbolic function. - -[source.merge.styled,esql] ----- -include::{esql-specs}/floats.csv-spec[tag=tanh] ----- -[%header.monospaced.styled,format=dsv,separator=|] -|=== -include::{esql-specs}/floats.csv-spec[tag=tanh-result] -|=== - -Supported types: - -include::types/tanh.asciidoc[] diff --git a/x-pack/plugins/elastic_assistant/server/knowledge_base/esql/documentation/functions/tau.asciidoc b/x-pack/plugins/elastic_assistant/server/knowledge_base/esql/documentation/functions/tau.asciidoc deleted file mode 100644 index 61f352b0db8de..0000000000000 --- a/x-pack/plugins/elastic_assistant/server/knowledge_base/esql/documentation/functions/tau.asciidoc +++ /dev/null @@ -1,16 +0,0 @@ -[discrete] -[[esql-tau]] -=== `TAU` -[.text-center] -image::esql/functions/signature/tau.svg[Embedded,opts=inline] - -The https://tauday.com/tau-manifesto[ratio] of a circle's circumference to its radius. - -[source.merge.styled,esql] ----- -include::{esql-specs}/math.csv-spec[tag=tau] ----- -[%header.monospaced.styled,format=dsv,separator=|] -|=== -include::{esql-specs}/math.csv-spec[tag=tau-result] -|=== diff --git a/x-pack/plugins/elastic_assistant/server/knowledge_base/esql/documentation/functions/to_boolean.asciidoc b/x-pack/plugins/elastic_assistant/server/knowledge_base/esql/documentation/functions/to_boolean.asciidoc deleted file mode 100644 index 03f21a503218c..0000000000000 --- a/x-pack/plugins/elastic_assistant/server/knowledge_base/esql/documentation/functions/to_boolean.asciidoc +++ /dev/null @@ -1,25 +0,0 @@ -[discrete] -[[esql-to_boolean]] -=== `TO_BOOLEAN` -Converts an input value to a boolean value. - -The input can be a single- or multi-valued field or an expression. The input -type must be of a string or numeric type. - -A string value of *"true"* will be case-insensitive converted to the Boolean -*true*. For anything else, including the empty string, the function will -return *false*. For example: - -[source.merge.styled,esql] ----- -include::{esql-specs}/boolean.csv-spec[tag=to_boolean] ----- -[%header.monospaced.styled,format=dsv,separator=|] -|=== -include::{esql-specs}/boolean.csv-spec[tag=to_boolean-result] -|=== - -The numerical value of *0* will be converted to *false*, anything else will be -converted to *true*. - -Alias: TO_BOOL diff --git a/x-pack/plugins/elastic_assistant/server/knowledge_base/esql/documentation/functions/to_datetime.asciidoc b/x-pack/plugins/elastic_assistant/server/knowledge_base/esql/documentation/functions/to_datetime.asciidoc deleted file mode 100644 index 750c8025cb6c2..0000000000000 --- a/x-pack/plugins/elastic_assistant/server/knowledge_base/esql/documentation/functions/to_datetime.asciidoc +++ /dev/null @@ -1,47 +0,0 @@ -[discrete] -[[esql-to_datetime]] -=== `TO_DATETIME` -Converts an input value to a date value. - -The input can be a single- or multi-valued field or an expression. The input -type must be of a string or numeric type. - -A string will only be successfully converted if it's respecting the format -`yyyy-MM-dd'T'HH:mm:ss.SSS'Z'` (to convert dates in other formats, use <>). For example: - -[source.merge.styled,esql] ----- -include::{esql-specs}/date.csv-spec[tag=to_datetime-str] ----- -[%header.monospaced.styled,format=dsv,separator=|] -|=== -include::{esql-specs}/date.csv-spec[tag=to_datetime-str-result] -|=== - -Note that in this example, the last value in the source multi-valued -field has not been converted. The reason being that if the date format is not -respected, the conversion will result in a *null* value. When this happens a -_Warning_ header is added to the response. The header will provide information -on the source of the failure: - -`"Line 1:112: evaluation of [TO_DATETIME(string)] failed, treating result as null. Only first 20 failures recorded."` - -A following header will contain the failure reason and the offending value: - -`"java.lang.IllegalArgumentException: failed to parse date field [1964-06-02 00:00:00] with format [yyyy-MM-dd'T'HH:mm:ss.SSS'Z']"` - - -If the input parameter is of a numeric type, its value will be interpreted as -milliseconds since the https://en.wikipedia.org/wiki/Unix_time[Unix epoch]. -For example: - -[source.merge.styled,esql] ----- -include::{esql-specs}/date.csv-spec[tag=to_datetime-int] ----- -[%header.monospaced.styled,format=dsv,separator=|] -|=== -include::{esql-specs}/date.csv-spec[tag=to_datetime-int-result] -|=== - -Alias: TO_DT diff --git a/x-pack/plugins/elastic_assistant/server/knowledge_base/esql/documentation/functions/to_degrees.asciidoc b/x-pack/plugins/elastic_assistant/server/knowledge_base/esql/documentation/functions/to_degrees.asciidoc deleted file mode 100644 index 71b480253fe35..0000000000000 --- a/x-pack/plugins/elastic_assistant/server/knowledge_base/esql/documentation/functions/to_degrees.asciidoc +++ /dev/null @@ -1,19 +0,0 @@ -[discrete] -[[esql-to_degrees]] -=== `TO_DEGREES` -Converts a number in https://en.wikipedia.org/wiki/Radian[radians] -to https://en.wikipedia.org/wiki/Degree_(angle)[degrees]. - -The input can be a single- or multi-valued field or an expression. The input -type must be of a numeric type and result is always `double`. - -Example: - -[source.merge.styled,esql] ----- -include::{esql-specs}/floats.csv-spec[tag=to_degrees] ----- -[%header.monospaced.styled,format=dsv,separator=|] -|=== -include::{esql-specs}/floats.csv-spec[tag=to_degrees-result] -|=== diff --git a/x-pack/plugins/elastic_assistant/server/knowledge_base/esql/documentation/functions/to_double.asciidoc b/x-pack/plugins/elastic_assistant/server/knowledge_base/esql/documentation/functions/to_double.asciidoc deleted file mode 100644 index 27ad84e4c7762..0000000000000 --- a/x-pack/plugins/elastic_assistant/server/knowledge_base/esql/documentation/functions/to_double.asciidoc +++ /dev/null @@ -1,38 +0,0 @@ -[discrete] -[[esql-to_double]] -=== `TO_DOUBLE` -Converts an input value to a double value. - -The input can be a single- or multi-valued field or an expression. The input -type must be of a boolean, date, string or numeric type. - -Example: - -[source.merge.styled,esql] ----- -include::{esql-specs}/floats.csv-spec[tag=to_double-str] ----- -[%header.monospaced.styled,format=dsv,separator=|] -|=== -include::{esql-specs}/floats.csv-spec[tag=to_double-str-result] -|=== - -Note that in this example, the last conversion of the string isn't -possible. When this happens, the result is a *null* value. In this case a -_Warning_ header is added to the response. The header will provide information -on the source of the failure: - -`"Line 1:115: evaluation of [TO_DOUBLE(str2)] failed, treating result as null. Only first 20 failures recorded."` - -A following header will contain the failure reason and the offending value: - -`"java.lang.NumberFormatException: For input string: \"foo\""` - - -If the input parameter is of a date type, its value will be interpreted as -milliseconds since the https://en.wikipedia.org/wiki/Unix_time[Unix epoch], -converted to double. - -Boolean *true* will be converted to double *1.0*, *false* to *0.0*. - -Alias: TO_DBL diff --git a/x-pack/plugins/elastic_assistant/server/knowledge_base/esql/documentation/functions/to_integer.asciidoc b/x-pack/plugins/elastic_assistant/server/knowledge_base/esql/documentation/functions/to_integer.asciidoc deleted file mode 100644 index e185b87d6d95d..0000000000000 --- a/x-pack/plugins/elastic_assistant/server/knowledge_base/esql/documentation/functions/to_integer.asciidoc +++ /dev/null @@ -1,38 +0,0 @@ -[discrete] -[[esql-to_integer]] -=== `TO_INTEGER` -Converts an input value to an integer value. - -The input can be a single- or multi-valued field or an expression. The input -type must be of a boolean, date, string or numeric type. - -Example: - -[source.merge.styled,esql] ----- -include::{esql-specs}/ints.csv-spec[tag=to_int-long] ----- -[%header.monospaced.styled,format=dsv,separator=|] -|=== -include::{esql-specs}/ints.csv-spec[tag=to_int-long-result] -|=== - -Note that in this example, the last value of the multi-valued field cannot -be converted as an integer. When this happens, the result is a *null* value. -In this case a _Warning_ header is added to the response. The header will -provide information on the source of the failure: - -`"Line 1:61: evaluation of [TO_INTEGER(long)] failed, treating result as null. Only first 20 failures recorded."` - -A following header will contain the failure reason and the offending value: - -`"org.elasticsearch.xpack.ql.QlIllegalArgumentException: [501379200000] out of [integer] range"` - - -If the input parameter is of a date type, its value will be interpreted as -milliseconds since the https://en.wikipedia.org/wiki/Unix_time[Unix epoch], -converted to integer. - -Boolean *true* will be converted to integer *1*, *false* to *0*. - -Alias: TO_INT diff --git a/x-pack/plugins/elastic_assistant/server/knowledge_base/esql/documentation/functions/to_ip.asciidoc b/x-pack/plugins/elastic_assistant/server/knowledge_base/esql/documentation/functions/to_ip.asciidoc deleted file mode 100644 index dea147eba1a41..0000000000000 --- a/x-pack/plugins/elastic_assistant/server/knowledge_base/esql/documentation/functions/to_ip.asciidoc +++ /dev/null @@ -1,28 +0,0 @@ -[discrete] -[[esql-to_ip]] -=== `TO_IP` -Converts an input string to an IP value. - -The input can be a single- or multi-valued field or an expression. - -Example: - -[source.merge.styled,esql] ----- -include::{esql-specs}/ip.csv-spec[tag=to_ip] ----- -[%header.monospaced.styled,format=dsv,separator=|] -|=== -include::{esql-specs}/ip.csv-spec[tag=to_ip-result] -|=== - -Note that in the example above the last conversion of the string isn't -possible. When this happens, the result is a *null* value. In this case a -_Warning_ header is added to the response. The header will provide information -on the source of the failure: - -`"Line 1:68: evaluation of [TO_IP(str2)] failed, treating result as null. Only first 20 failures recorded."` - -A following header will contain the failure reason and the offending value: - -`"java.lang.IllegalArgumentException: 'foo' is not an IP string literal."` diff --git a/x-pack/plugins/elastic_assistant/server/knowledge_base/esql/documentation/functions/to_long.asciidoc b/x-pack/plugins/elastic_assistant/server/knowledge_base/esql/documentation/functions/to_long.asciidoc deleted file mode 100644 index 9501c28a31657..0000000000000 --- a/x-pack/plugins/elastic_assistant/server/knowledge_base/esql/documentation/functions/to_long.asciidoc +++ /dev/null @@ -1,36 +0,0 @@ -[discrete] -[[esql-to_long]] -=== `TO_LONG` -Converts an input value to a long value. - -The input can be a single- or multi-valued field or an expression. The input -type must be of a boolean, date, string or numeric type. - -Example: - -[source.merge.styled,esql] ----- -include::{esql-specs}/ints.csv-spec[tag=to_long-str] ----- -[%header.monospaced.styled,format=dsv,separator=|] -|=== -include::{esql-specs}/ints.csv-spec[tag=to_long-str-result] -|=== - -Note that in this example, the last conversion of the string isn't -possible. When this happens, the result is a *null* value. In this case a -_Warning_ header is added to the response. The header will provide information -on the source of the failure: - -`"Line 1:113: evaluation of [TO_LONG(str3)] failed, treating result as null. Only first 20 failures recorded."` - -A following header will contain the failure reason and the offending value: - -`"java.lang.NumberFormatException: For input string: \"foo\""` - - -If the input parameter is of a date type, its value will be interpreted as -milliseconds since the https://en.wikipedia.org/wiki/Unix_time[Unix epoch], -converted to long. - -Boolean *true* will be converted to long *1*, *false* to *0*. diff --git a/x-pack/plugins/elastic_assistant/server/knowledge_base/esql/documentation/functions/to_radians.asciidoc b/x-pack/plugins/elastic_assistant/server/knowledge_base/esql/documentation/functions/to_radians.asciidoc deleted file mode 100644 index 1f86f1fb983cc..0000000000000 --- a/x-pack/plugins/elastic_assistant/server/knowledge_base/esql/documentation/functions/to_radians.asciidoc +++ /dev/null @@ -1,19 +0,0 @@ -[discrete] -[[esql-to_radians]] -=== `TO_RADIANS` -Converts a number in https://en.wikipedia.org/wiki/Degree_(angle)[degrees] to -https://en.wikipedia.org/wiki/Radian[radians]. - -The input can be a single- or multi-valued field or an expression. The input -type must be of a numeric type and result is always `double`. - -Example: - -[source.merge.styled,esql] ----- -include::{esql-specs}/floats.csv-spec[tag=to_radians] ----- -[%header.monospaced.styled,format=dsv,separator=|] -|=== -include::{esql-specs}/floats.csv-spec[tag=to_radians-result] -|=== diff --git a/x-pack/plugins/elastic_assistant/server/knowledge_base/esql/documentation/functions/to_string.asciidoc b/x-pack/plugins/elastic_assistant/server/knowledge_base/esql/documentation/functions/to_string.asciidoc deleted file mode 100644 index d03b6511b8de5..0000000000000 --- a/x-pack/plugins/elastic_assistant/server/knowledge_base/esql/documentation/functions/to_string.asciidoc +++ /dev/null @@ -1,33 +0,0 @@ -[discrete] -[[esql-to_string]] -=== `TO_STRING` -[.text-center] -image::esql/functions/signature/to_string.svg[Embedded,opts=inline] - -Converts a field into a string. For example: - -[source.merge.styled,esql] ----- -include::{esql-specs}/string.csv-spec[tag=to_string] ----- -[%header.monospaced.styled,format=dsv,separator=|] -|=== -include::{esql-specs}/string.csv-spec[tag=to_string-result] -|=== - -It also works fine on multivalued fields: - -[source.merge.styled,esql] ----- -include::{esql-specs}/string.csv-spec[tag=to_string_multivalue] ----- -[%header.monospaced.styled,format=dsv,separator=|] -|=== -include::{esql-specs}/string.csv-spec[tag=to_string_multivalue-result] -|=== - -Alias: TO_STR - -Supported types: - -include::types/to_string.asciidoc[] diff --git a/x-pack/plugins/elastic_assistant/server/knowledge_base/esql/documentation/functions/to_unsigned_long.asciidoc b/x-pack/plugins/elastic_assistant/server/knowledge_base/esql/documentation/functions/to_unsigned_long.asciidoc deleted file mode 100644 index af3ff05bf055c..0000000000000 --- a/x-pack/plugins/elastic_assistant/server/knowledge_base/esql/documentation/functions/to_unsigned_long.asciidoc +++ /dev/null @@ -1,38 +0,0 @@ -[discrete] -[[esql-to_unsigned_long]] -=== `TO_UNSIGNED_LONG` -Converts an input value to an unsigned long value. - -The input can be a single- or multi-valued field or an expression. The input -type must be of a boolean, date, string or numeric type. - -Example: - -[source.merge.styled,esql] ----- -include::{esql-specs}/ints.csv-spec[tag=to_unsigned_long-str] ----- -[%header.monospaced.styled,format=dsv,separator=|] -|=== -include::{esql-specs}/ints.csv-spec[tag=to_unsigned_long-str-result] -|=== - -Note that in this example, the last conversion of the string isn't -possible. When this happens, the result is a *null* value. In this case a -_Warning_ header is added to the response. The header will provide information -on the source of the failure: - -`"Line 1:133: evaluation of [TO_UL(str3)] failed, treating result as null. Only first 20 failures recorded."` - -A following header will contain the failure reason and the offending value: - -`"java.lang.NumberFormatException: Character f is neither a decimal digit number, decimal point, nor \"e\" notation exponential mark."` - - -If the input parameter is of a date type, its value will be interpreted as -milliseconds since the https://en.wikipedia.org/wiki/Unix_time[Unix epoch], -converted to unsigned long. - -Boolean *true* will be converted to unsigned long *1*, *false* to *0*. - -Alias: TO_ULONG, TO_UL diff --git a/x-pack/plugins/elastic_assistant/server/knowledge_base/esql/documentation/functions/to_version.asciidoc b/x-pack/plugins/elastic_assistant/server/knowledge_base/esql/documentation/functions/to_version.asciidoc deleted file mode 100644 index 33419233c4788..0000000000000 --- a/x-pack/plugins/elastic_assistant/server/knowledge_base/esql/documentation/functions/to_version.asciidoc +++ /dev/null @@ -1,24 +0,0 @@ -[discrete] -[[esql-to_version]] -=== `TO_VERSION` -[.text-center] -image::esql/functions/signature/to_version.svg[Embedded,opts=inline] - -Converts an input string to a version value. For example: - -[source.merge.styled,esql] ----- -include::{esql-specs}/version.csv-spec[tag=to_version] ----- -[%header.monospaced.styled,format=dsv,separator=|] -|=== -include::{esql-specs}/version.csv-spec[tag=to_version-result] -|=== - -The input can be a single- or multi-valued field or an expression. - -Alias: TO_VER - -Supported types: - -include::types/to_version.asciidoc[] diff --git a/x-pack/plugins/elastic_assistant/server/knowledge_base/esql/documentation/functions/trim.asciidoc b/x-pack/plugins/elastic_assistant/server/knowledge_base/esql/documentation/functions/trim.asciidoc deleted file mode 100644 index 6ace6118dd757..0000000000000 --- a/x-pack/plugins/elastic_assistant/server/knowledge_base/esql/documentation/functions/trim.asciidoc +++ /dev/null @@ -1,20 +0,0 @@ -[discrete] -[[esql-trim]] -=== `TRIM` -[.text-center] -image::esql/functions/signature/trim.svg[Embedded,opts=inline] - -Removes leading and trailing whitespaces from strings. - -[source.merge.styled,esql] ----- -include::{esql-specs}/string.csv-spec[tag=trim] ----- -[%header.monospaced.styled,format=dsv,separator=|] -|=== -include::{esql-specs}/string.csv-spec[tag=trim-result] -|=== - -Supported types: - -include::types/trim.asciidoc[] diff --git a/x-pack/plugins/elastic_assistant/server/knowledge_base/esql/documentation/functions/type_conversion_functions.asciidoc b/x-pack/plugins/elastic_assistant/server/knowledge_base/esql/documentation/functions/type_conversion_functions.asciidoc deleted file mode 100644 index 640006c936526..0000000000000 --- a/x-pack/plugins/elastic_assistant/server/knowledge_base/esql/documentation/functions/type_conversion_functions.asciidoc +++ /dev/null @@ -1,34 +0,0 @@ -[[esql-type-conversion-functions]] -==== {esql} type conversion functions - -++++ -Type conversion functions -++++ - -{esql} supports these type conversion functions: - -// tag::type_list[] -* <> -* <> -* <> -* <> -* <> -* <> -* <> -* <> -* <> -* <> -* <> -// end::type_list[] - -include::to_boolean.asciidoc[] -include::to_datetime.asciidoc[] -include::to_degrees.asciidoc[] -include::to_double.asciidoc[] -include::to_integer.asciidoc[] -include::to_ip.asciidoc[] -include::to_long.asciidoc[] -include::to_radians.asciidoc[] -include::to_string.asciidoc[] -include::to_unsigned_long.asciidoc[] -include::to_version.asciidoc[] diff --git a/x-pack/plugins/elastic_assistant/server/knowledge_base/esql/documentation/functions/types/abs.asciidoc b/x-pack/plugins/elastic_assistant/server/knowledge_base/esql/documentation/functions/types/abs.asciidoc deleted file mode 100644 index 54341360fed3f..0000000000000 --- a/x-pack/plugins/elastic_assistant/server/knowledge_base/esql/documentation/functions/types/abs.asciidoc +++ /dev/null @@ -1,8 +0,0 @@ -[%header.monospaced.styled,format=dsv,separator=|] -|=== -n | result -double | double -integer | integer -long | long -unsigned_long | unsigned_long -|=== diff --git a/x-pack/plugins/elastic_assistant/server/knowledge_base/esql/documentation/functions/types/acos.asciidoc b/x-pack/plugins/elastic_assistant/server/knowledge_base/esql/documentation/functions/types/acos.asciidoc deleted file mode 100644 index 1df8dd6526f18..0000000000000 --- a/x-pack/plugins/elastic_assistant/server/knowledge_base/esql/documentation/functions/types/acos.asciidoc +++ /dev/null @@ -1,8 +0,0 @@ -[%header.monospaced.styled,format=dsv,separator=|] -|=== -n | result -double | double -integer | double -long | double -unsigned_long | double -|=== diff --git a/x-pack/plugins/elastic_assistant/server/knowledge_base/esql/documentation/functions/types/asin.asciidoc b/x-pack/plugins/elastic_assistant/server/knowledge_base/esql/documentation/functions/types/asin.asciidoc deleted file mode 100644 index 1df8dd6526f18..0000000000000 --- a/x-pack/plugins/elastic_assistant/server/knowledge_base/esql/documentation/functions/types/asin.asciidoc +++ /dev/null @@ -1,8 +0,0 @@ -[%header.monospaced.styled,format=dsv,separator=|] -|=== -n | result -double | double -integer | double -long | double -unsigned_long | double -|=== diff --git a/x-pack/plugins/elastic_assistant/server/knowledge_base/esql/documentation/functions/types/atan.asciidoc b/x-pack/plugins/elastic_assistant/server/knowledge_base/esql/documentation/functions/types/atan.asciidoc deleted file mode 100644 index 1df8dd6526f18..0000000000000 --- a/x-pack/plugins/elastic_assistant/server/knowledge_base/esql/documentation/functions/types/atan.asciidoc +++ /dev/null @@ -1,8 +0,0 @@ -[%header.monospaced.styled,format=dsv,separator=|] -|=== -n | result -double | double -integer | double -long | double -unsigned_long | double -|=== diff --git a/x-pack/plugins/elastic_assistant/server/knowledge_base/esql/documentation/functions/types/atan2.asciidoc b/x-pack/plugins/elastic_assistant/server/knowledge_base/esql/documentation/functions/types/atan2.asciidoc deleted file mode 100644 index 74fffe9056a16..0000000000000 --- a/x-pack/plugins/elastic_assistant/server/knowledge_base/esql/documentation/functions/types/atan2.asciidoc +++ /dev/null @@ -1,20 +0,0 @@ -[%header.monospaced.styled,format=dsv,separator=|] -|=== -y | x | result -double | double | double -double | integer | double -double | long | double -double | unsigned_long | double -integer | double | double -integer | integer | double -integer | long | double -integer | unsigned_long | double -long | double | double -long | integer | double -long | long | double -long | unsigned_long | double -unsigned_long | double | double -unsigned_long | integer | double -unsigned_long | long | double -unsigned_long | unsigned_long | double -|=== diff --git a/x-pack/plugins/elastic_assistant/server/knowledge_base/esql/documentation/functions/types/auto_bucket.asciidoc b/x-pack/plugins/elastic_assistant/server/knowledge_base/esql/documentation/functions/types/auto_bucket.asciidoc deleted file mode 100644 index d2f134b99fbb0..0000000000000 --- a/x-pack/plugins/elastic_assistant/server/knowledge_base/esql/documentation/functions/types/auto_bucket.asciidoc +++ /dev/null @@ -1,5 +0,0 @@ -[%header.monospaced.styled,format=dsv,separator=|] -|=== -arg1 | arg2 | arg3 | arg4 | result - -|=== diff --git a/x-pack/plugins/elastic_assistant/server/knowledge_base/esql/documentation/functions/types/case.asciidoc b/x-pack/plugins/elastic_assistant/server/knowledge_base/esql/documentation/functions/types/case.asciidoc deleted file mode 100644 index 7062d7000115a..0000000000000 --- a/x-pack/plugins/elastic_assistant/server/knowledge_base/esql/documentation/functions/types/case.asciidoc +++ /dev/null @@ -1,5 +0,0 @@ -[%header.monospaced.styled,format=dsv,separator=|] -|=== -arg1 | arg2 | result - -|=== diff --git a/x-pack/plugins/elastic_assistant/server/knowledge_base/esql/documentation/functions/types/ceil.asciidoc b/x-pack/plugins/elastic_assistant/server/knowledge_base/esql/documentation/functions/types/ceil.asciidoc deleted file mode 100644 index 54341360fed3f..0000000000000 --- a/x-pack/plugins/elastic_assistant/server/knowledge_base/esql/documentation/functions/types/ceil.asciidoc +++ /dev/null @@ -1,8 +0,0 @@ -[%header.monospaced.styled,format=dsv,separator=|] -|=== -n | result -double | double -integer | integer -long | long -unsigned_long | unsigned_long -|=== diff --git a/x-pack/plugins/elastic_assistant/server/knowledge_base/esql/documentation/functions/types/coalesce.asciidoc b/x-pack/plugins/elastic_assistant/server/knowledge_base/esql/documentation/functions/types/coalesce.asciidoc deleted file mode 100644 index e36316ab87bb5..0000000000000 --- a/x-pack/plugins/elastic_assistant/server/knowledge_base/esql/documentation/functions/types/coalesce.asciidoc +++ /dev/null @@ -1,9 +0,0 @@ -[%header.monospaced.styled,format=dsv,separator=|] -|=== -arg1 | arg2 | result -boolean | boolean | boolean -integer | integer | integer -keyword | keyword | keyword -long | long | long -text | text | text -|=== diff --git a/x-pack/plugins/elastic_assistant/server/knowledge_base/esql/documentation/functions/types/concat.asciidoc b/x-pack/plugins/elastic_assistant/server/knowledge_base/esql/documentation/functions/types/concat.asciidoc deleted file mode 100644 index f422b45f0b34c..0000000000000 --- a/x-pack/plugins/elastic_assistant/server/knowledge_base/esql/documentation/functions/types/concat.asciidoc +++ /dev/null @@ -1,6 +0,0 @@ -[%header.monospaced.styled,format=dsv,separator=|] -|=== -arg1 | arg2 | result -keyword | keyword | keyword -text | text | keyword -|=== diff --git a/x-pack/plugins/elastic_assistant/server/knowledge_base/esql/documentation/functions/types/cos.asciidoc b/x-pack/plugins/elastic_assistant/server/knowledge_base/esql/documentation/functions/types/cos.asciidoc deleted file mode 100644 index 1df8dd6526f18..0000000000000 --- a/x-pack/plugins/elastic_assistant/server/knowledge_base/esql/documentation/functions/types/cos.asciidoc +++ /dev/null @@ -1,8 +0,0 @@ -[%header.monospaced.styled,format=dsv,separator=|] -|=== -n | result -double | double -integer | double -long | double -unsigned_long | double -|=== diff --git a/x-pack/plugins/elastic_assistant/server/knowledge_base/esql/documentation/functions/types/cosh.asciidoc b/x-pack/plugins/elastic_assistant/server/knowledge_base/esql/documentation/functions/types/cosh.asciidoc deleted file mode 100644 index 1df8dd6526f18..0000000000000 --- a/x-pack/plugins/elastic_assistant/server/knowledge_base/esql/documentation/functions/types/cosh.asciidoc +++ /dev/null @@ -1,8 +0,0 @@ -[%header.monospaced.styled,format=dsv,separator=|] -|=== -n | result -double | double -integer | double -long | double -unsigned_long | double -|=== diff --git a/x-pack/plugins/elastic_assistant/server/knowledge_base/esql/documentation/functions/types/date_extract.asciidoc b/x-pack/plugins/elastic_assistant/server/knowledge_base/esql/documentation/functions/types/date_extract.asciidoc deleted file mode 100644 index 9963c85b2af85..0000000000000 --- a/x-pack/plugins/elastic_assistant/server/knowledge_base/esql/documentation/functions/types/date_extract.asciidoc +++ /dev/null @@ -1,5 +0,0 @@ -[%header.monospaced.styled,format=dsv,separator=|] -|=== -arg1 | arg2 | result -keyword | datetime | long -|=== diff --git a/x-pack/plugins/elastic_assistant/server/knowledge_base/esql/documentation/functions/types/date_parse.asciidoc b/x-pack/plugins/elastic_assistant/server/knowledge_base/esql/documentation/functions/types/date_parse.asciidoc deleted file mode 100644 index f4922b9bf9c61..0000000000000 --- a/x-pack/plugins/elastic_assistant/server/knowledge_base/esql/documentation/functions/types/date_parse.asciidoc +++ /dev/null @@ -1,6 +0,0 @@ -[%header.monospaced.styled,format=dsv,separator=|] -|=== -datePattern | dateString | result -keyword | keyword | datetime -keyword | text | datetime -|=== diff --git a/x-pack/plugins/elastic_assistant/server/knowledge_base/esql/documentation/functions/types/e.asciidoc b/x-pack/plugins/elastic_assistant/server/knowledge_base/esql/documentation/functions/types/e.asciidoc deleted file mode 100644 index 5854465d5fb49..0000000000000 --- a/x-pack/plugins/elastic_assistant/server/knowledge_base/esql/documentation/functions/types/e.asciidoc +++ /dev/null @@ -1,5 +0,0 @@ -[%header.monospaced.styled,format=dsv,separator=|] -|=== -result - -|=== diff --git a/x-pack/plugins/elastic_assistant/server/knowledge_base/esql/documentation/functions/types/ends_with.asciidoc b/x-pack/plugins/elastic_assistant/server/knowledge_base/esql/documentation/functions/types/ends_with.asciidoc deleted file mode 100644 index 6c406b80c0cad..0000000000000 --- a/x-pack/plugins/elastic_assistant/server/knowledge_base/esql/documentation/functions/types/ends_with.asciidoc +++ /dev/null @@ -1,5 +0,0 @@ -[%header.monospaced.styled,format=dsv,separator=|] -|=== -arg1 | arg2 | result -keyword | keyword | boolean -|=== diff --git a/x-pack/plugins/elastic_assistant/server/knowledge_base/esql/documentation/functions/types/floor.asciidoc b/x-pack/plugins/elastic_assistant/server/knowledge_base/esql/documentation/functions/types/floor.asciidoc deleted file mode 100644 index 54341360fed3f..0000000000000 --- a/x-pack/plugins/elastic_assistant/server/knowledge_base/esql/documentation/functions/types/floor.asciidoc +++ /dev/null @@ -1,8 +0,0 @@ -[%header.monospaced.styled,format=dsv,separator=|] -|=== -n | result -double | double -integer | integer -long | long -unsigned_long | unsigned_long -|=== diff --git a/x-pack/plugins/elastic_assistant/server/knowledge_base/esql/documentation/functions/types/greatest.asciidoc b/x-pack/plugins/elastic_assistant/server/knowledge_base/esql/documentation/functions/types/greatest.asciidoc deleted file mode 100644 index 0e4ebb2d45a31..0000000000000 --- a/x-pack/plugins/elastic_assistant/server/knowledge_base/esql/documentation/functions/types/greatest.asciidoc +++ /dev/null @@ -1,12 +0,0 @@ -[%header.monospaced.styled,format=dsv,separator=|] -|=== -first | rest | result -boolean | boolean | boolean -double | double | double -integer | integer | integer -ip | ip | ip -keyword | keyword | keyword -long | long | long -text | text | text -version | version | version -|=== diff --git a/x-pack/plugins/elastic_assistant/server/knowledge_base/esql/documentation/functions/types/is_finite.asciidoc b/x-pack/plugins/elastic_assistant/server/knowledge_base/esql/documentation/functions/types/is_finite.asciidoc deleted file mode 100644 index 0c555059004c1..0000000000000 --- a/x-pack/plugins/elastic_assistant/server/knowledge_base/esql/documentation/functions/types/is_finite.asciidoc +++ /dev/null @@ -1,5 +0,0 @@ -[%header.monospaced.styled,format=dsv,separator=|] -|=== -arg1 | result -double | boolean -|=== diff --git a/x-pack/plugins/elastic_assistant/server/knowledge_base/esql/documentation/functions/types/is_infinite.asciidoc b/x-pack/plugins/elastic_assistant/server/knowledge_base/esql/documentation/functions/types/is_infinite.asciidoc deleted file mode 100644 index 0c555059004c1..0000000000000 --- a/x-pack/plugins/elastic_assistant/server/knowledge_base/esql/documentation/functions/types/is_infinite.asciidoc +++ /dev/null @@ -1,5 +0,0 @@ -[%header.monospaced.styled,format=dsv,separator=|] -|=== -arg1 | result -double | boolean -|=== diff --git a/x-pack/plugins/elastic_assistant/server/knowledge_base/esql/documentation/functions/types/least.asciidoc b/x-pack/plugins/elastic_assistant/server/knowledge_base/esql/documentation/functions/types/least.asciidoc deleted file mode 100644 index 0e4ebb2d45a31..0000000000000 --- a/x-pack/plugins/elastic_assistant/server/knowledge_base/esql/documentation/functions/types/least.asciidoc +++ /dev/null @@ -1,12 +0,0 @@ -[%header.monospaced.styled,format=dsv,separator=|] -|=== -first | rest | result -boolean | boolean | boolean -double | double | double -integer | integer | integer -ip | ip | ip -keyword | keyword | keyword -long | long | long -text | text | text -version | version | version -|=== diff --git a/x-pack/plugins/elastic_assistant/server/knowledge_base/esql/documentation/functions/types/left.asciidoc b/x-pack/plugins/elastic_assistant/server/knowledge_base/esql/documentation/functions/types/left.asciidoc deleted file mode 100644 index c30a055f3be49..0000000000000 --- a/x-pack/plugins/elastic_assistant/server/knowledge_base/esql/documentation/functions/types/left.asciidoc +++ /dev/null @@ -1,5 +0,0 @@ -[%header.monospaced.styled,format=dsv,separator=|] -|=== -string | length | result -keyword | integer | keyword -|=== diff --git a/x-pack/plugins/elastic_assistant/server/knowledge_base/esql/documentation/functions/types/length.asciidoc b/x-pack/plugins/elastic_assistant/server/knowledge_base/esql/documentation/functions/types/length.asciidoc deleted file mode 100644 index 9af62defcb2a9..0000000000000 --- a/x-pack/plugins/elastic_assistant/server/knowledge_base/esql/documentation/functions/types/length.asciidoc +++ /dev/null @@ -1,5 +0,0 @@ -[%header.monospaced.styled,format=dsv,separator=|] -|=== -arg1 | result -keyword | integer -|=== diff --git a/x-pack/plugins/elastic_assistant/server/knowledge_base/esql/documentation/functions/types/log10.asciidoc b/x-pack/plugins/elastic_assistant/server/knowledge_base/esql/documentation/functions/types/log10.asciidoc deleted file mode 100644 index 1df8dd6526f18..0000000000000 --- a/x-pack/plugins/elastic_assistant/server/knowledge_base/esql/documentation/functions/types/log10.asciidoc +++ /dev/null @@ -1,8 +0,0 @@ -[%header.monospaced.styled,format=dsv,separator=|] -|=== -n | result -double | double -integer | double -long | double -unsigned_long | double -|=== diff --git a/x-pack/plugins/elastic_assistant/server/knowledge_base/esql/documentation/functions/types/ltrim.asciidoc b/x-pack/plugins/elastic_assistant/server/knowledge_base/esql/documentation/functions/types/ltrim.asciidoc deleted file mode 100644 index 11c02c8f0c3bb..0000000000000 --- a/x-pack/plugins/elastic_assistant/server/knowledge_base/esql/documentation/functions/types/ltrim.asciidoc +++ /dev/null @@ -1,6 +0,0 @@ -[%header.monospaced.styled,format=dsv,separator=|] -|=== -arg1 | result -keyword | keyword -text | text -|=== diff --git a/x-pack/plugins/elastic_assistant/server/knowledge_base/esql/documentation/functions/types/mv_avg.asciidoc b/x-pack/plugins/elastic_assistant/server/knowledge_base/esql/documentation/functions/types/mv_avg.asciidoc deleted file mode 100644 index dd4f6b0725cc8..0000000000000 --- a/x-pack/plugins/elastic_assistant/server/knowledge_base/esql/documentation/functions/types/mv_avg.asciidoc +++ /dev/null @@ -1,8 +0,0 @@ -[%header.monospaced.styled,format=dsv,separator=|] -|=== -arg1 | result -double | double -integer | double -long | double -unsigned_long | double -|=== diff --git a/x-pack/plugins/elastic_assistant/server/knowledge_base/esql/documentation/functions/types/mv_concat.asciidoc b/x-pack/plugins/elastic_assistant/server/knowledge_base/esql/documentation/functions/types/mv_concat.asciidoc deleted file mode 100644 index 2836799f335e8..0000000000000 --- a/x-pack/plugins/elastic_assistant/server/knowledge_base/esql/documentation/functions/types/mv_concat.asciidoc +++ /dev/null @@ -1,8 +0,0 @@ -[%header.monospaced.styled,format=dsv,separator=|] -|=== -arg1 | arg2 | result -keyword | keyword | keyword -keyword | text | keyword -text | keyword | keyword -text | text | keyword -|=== diff --git a/x-pack/plugins/elastic_assistant/server/knowledge_base/esql/documentation/functions/types/mv_count.asciidoc b/x-pack/plugins/elastic_assistant/server/knowledge_base/esql/documentation/functions/types/mv_count.asciidoc deleted file mode 100644 index 2fcdfc65fa63b..0000000000000 --- a/x-pack/plugins/elastic_assistant/server/knowledge_base/esql/documentation/functions/types/mv_count.asciidoc +++ /dev/null @@ -1,10 +0,0 @@ -[%header.monospaced.styled,format=dsv,separator=|] -|=== -arg1 | result -boolean | integer -double | integer -integer | integer -keyword | integer -long | integer -unsigned_long | integer -|=== diff --git a/x-pack/plugins/elastic_assistant/server/knowledge_base/esql/documentation/functions/types/mv_dedupe.asciidoc b/x-pack/plugins/elastic_assistant/server/knowledge_base/esql/documentation/functions/types/mv_dedupe.asciidoc deleted file mode 100644 index 4e12c68422662..0000000000000 --- a/x-pack/plugins/elastic_assistant/server/knowledge_base/esql/documentation/functions/types/mv_dedupe.asciidoc +++ /dev/null @@ -1,9 +0,0 @@ -[%header.monospaced.styled,format=dsv,separator=|] -|=== -arg1 | result -boolean | boolean -double | double -integer | integer -keyword | keyword -long | long -|=== diff --git a/x-pack/plugins/elastic_assistant/server/knowledge_base/esql/documentation/functions/types/mv_max.asciidoc b/x-pack/plugins/elastic_assistant/server/knowledge_base/esql/documentation/functions/types/mv_max.asciidoc deleted file mode 100644 index 50740a71e4b49..0000000000000 --- a/x-pack/plugins/elastic_assistant/server/knowledge_base/esql/documentation/functions/types/mv_max.asciidoc +++ /dev/null @@ -1,10 +0,0 @@ -[%header.monospaced.styled,format=dsv,separator=|] -|=== -arg1 | result -boolean | boolean -double | double -integer | integer -keyword | keyword -long | long -unsigned_long | unsigned_long -|=== diff --git a/x-pack/plugins/elastic_assistant/server/knowledge_base/esql/documentation/functions/types/mv_median.asciidoc b/x-pack/plugins/elastic_assistant/server/knowledge_base/esql/documentation/functions/types/mv_median.asciidoc deleted file mode 100644 index f1831429aa95c..0000000000000 --- a/x-pack/plugins/elastic_assistant/server/knowledge_base/esql/documentation/functions/types/mv_median.asciidoc +++ /dev/null @@ -1,8 +0,0 @@ -[%header.monospaced.styled,format=dsv,separator=|] -|=== -arg1 | result -double | double -integer | integer -long | long -unsigned_long | unsigned_long -|=== diff --git a/x-pack/plugins/elastic_assistant/server/knowledge_base/esql/documentation/functions/types/mv_min.asciidoc b/x-pack/plugins/elastic_assistant/server/knowledge_base/esql/documentation/functions/types/mv_min.asciidoc deleted file mode 100644 index 50740a71e4b49..0000000000000 --- a/x-pack/plugins/elastic_assistant/server/knowledge_base/esql/documentation/functions/types/mv_min.asciidoc +++ /dev/null @@ -1,10 +0,0 @@ -[%header.monospaced.styled,format=dsv,separator=|] -|=== -arg1 | result -boolean | boolean -double | double -integer | integer -keyword | keyword -long | long -unsigned_long | unsigned_long -|=== diff --git a/x-pack/plugins/elastic_assistant/server/knowledge_base/esql/documentation/functions/types/mv_sum.asciidoc b/x-pack/plugins/elastic_assistant/server/knowledge_base/esql/documentation/functions/types/mv_sum.asciidoc deleted file mode 100644 index 09cb78511d275..0000000000000 --- a/x-pack/plugins/elastic_assistant/server/knowledge_base/esql/documentation/functions/types/mv_sum.asciidoc +++ /dev/null @@ -1,5 +0,0 @@ -[%header.monospaced.styled,format=dsv,separator=|] -|=== -arg1 | result -double | double -|=== diff --git a/x-pack/plugins/elastic_assistant/server/knowledge_base/esql/documentation/functions/types/pi.asciidoc b/x-pack/plugins/elastic_assistant/server/knowledge_base/esql/documentation/functions/types/pi.asciidoc deleted file mode 100644 index 5854465d5fb49..0000000000000 --- a/x-pack/plugins/elastic_assistant/server/knowledge_base/esql/documentation/functions/types/pi.asciidoc +++ /dev/null @@ -1,5 +0,0 @@ -[%header.monospaced.styled,format=dsv,separator=|] -|=== -result - -|=== diff --git a/x-pack/plugins/elastic_assistant/server/knowledge_base/esql/documentation/functions/types/pow.asciidoc b/x-pack/plugins/elastic_assistant/server/knowledge_base/esql/documentation/functions/types/pow.asciidoc deleted file mode 100644 index 37bddc60c118f..0000000000000 --- a/x-pack/plugins/elastic_assistant/server/knowledge_base/esql/documentation/functions/types/pow.asciidoc +++ /dev/null @@ -1,10 +0,0 @@ -[%header.monospaced.styled,format=dsv,separator=|] -|=== -base | exponent | result -double | double | double -double | integer | double -integer | double | double -integer | integer | integer -long | double | double -long | integer | long -|=== diff --git a/x-pack/plugins/elastic_assistant/server/knowledge_base/esql/documentation/functions/types/replace.asciidoc b/x-pack/plugins/elastic_assistant/server/knowledge_base/esql/documentation/functions/types/replace.asciidoc deleted file mode 100644 index 6824d1fd97128..0000000000000 --- a/x-pack/plugins/elastic_assistant/server/knowledge_base/esql/documentation/functions/types/replace.asciidoc +++ /dev/null @@ -1,12 +0,0 @@ -[%header.monospaced.styled,format=dsv,separator=|] -|=== -arg1 | arg2 | arg3 | result -keyword | keyword | keyword | keyword -keyword | keyword | text | keyword -keyword | text | keyword | keyword -keyword | text | text | keyword -text | keyword | keyword | keyword -text | keyword | text | keyword -text | text | keyword | keyword -text | text | text | keyword -|=== diff --git a/x-pack/plugins/elastic_assistant/server/knowledge_base/esql/documentation/functions/types/right.asciidoc b/x-pack/plugins/elastic_assistant/server/knowledge_base/esql/documentation/functions/types/right.asciidoc deleted file mode 100644 index c30a055f3be49..0000000000000 --- a/x-pack/plugins/elastic_assistant/server/knowledge_base/esql/documentation/functions/types/right.asciidoc +++ /dev/null @@ -1,5 +0,0 @@ -[%header.monospaced.styled,format=dsv,separator=|] -|=== -string | length | result -keyword | integer | keyword -|=== diff --git a/x-pack/plugins/elastic_assistant/server/knowledge_base/esql/documentation/functions/types/round.asciidoc b/x-pack/plugins/elastic_assistant/server/knowledge_base/esql/documentation/functions/types/round.asciidoc deleted file mode 100644 index 5ba9e2f776d75..0000000000000 --- a/x-pack/plugins/elastic_assistant/server/knowledge_base/esql/documentation/functions/types/round.asciidoc +++ /dev/null @@ -1,5 +0,0 @@ -[%header.monospaced.styled,format=dsv,separator=|] -|=== -arg1 | arg2 | result -double | integer | double -|=== diff --git a/x-pack/plugins/elastic_assistant/server/knowledge_base/esql/documentation/functions/types/rtrim.asciidoc b/x-pack/plugins/elastic_assistant/server/knowledge_base/esql/documentation/functions/types/rtrim.asciidoc deleted file mode 100644 index 11c02c8f0c3bb..0000000000000 --- a/x-pack/plugins/elastic_assistant/server/knowledge_base/esql/documentation/functions/types/rtrim.asciidoc +++ /dev/null @@ -1,6 +0,0 @@ -[%header.monospaced.styled,format=dsv,separator=|] -|=== -arg1 | result -keyword | keyword -text | text -|=== diff --git a/x-pack/plugins/elastic_assistant/server/knowledge_base/esql/documentation/functions/types/sin.asciidoc b/x-pack/plugins/elastic_assistant/server/knowledge_base/esql/documentation/functions/types/sin.asciidoc deleted file mode 100644 index 1df8dd6526f18..0000000000000 --- a/x-pack/plugins/elastic_assistant/server/knowledge_base/esql/documentation/functions/types/sin.asciidoc +++ /dev/null @@ -1,8 +0,0 @@ -[%header.monospaced.styled,format=dsv,separator=|] -|=== -n | result -double | double -integer | double -long | double -unsigned_long | double -|=== diff --git a/x-pack/plugins/elastic_assistant/server/knowledge_base/esql/documentation/functions/types/sinh.asciidoc b/x-pack/plugins/elastic_assistant/server/knowledge_base/esql/documentation/functions/types/sinh.asciidoc deleted file mode 100644 index 1df8dd6526f18..0000000000000 --- a/x-pack/plugins/elastic_assistant/server/knowledge_base/esql/documentation/functions/types/sinh.asciidoc +++ /dev/null @@ -1,8 +0,0 @@ -[%header.monospaced.styled,format=dsv,separator=|] -|=== -n | result -double | double -integer | double -long | double -unsigned_long | double -|=== diff --git a/x-pack/plugins/elastic_assistant/server/knowledge_base/esql/documentation/functions/types/split.asciidoc b/x-pack/plugins/elastic_assistant/server/knowledge_base/esql/documentation/functions/types/split.asciidoc deleted file mode 100644 index f1f744dbe4126..0000000000000 --- a/x-pack/plugins/elastic_assistant/server/knowledge_base/esql/documentation/functions/types/split.asciidoc +++ /dev/null @@ -1,5 +0,0 @@ -[%header.monospaced.styled,format=dsv,separator=|] -|=== -arg1 | arg2 | result -keyword | keyword | keyword -|=== diff --git a/x-pack/plugins/elastic_assistant/server/knowledge_base/esql/documentation/functions/types/sqrt.asciidoc b/x-pack/plugins/elastic_assistant/server/knowledge_base/esql/documentation/functions/types/sqrt.asciidoc deleted file mode 100644 index 1df8dd6526f18..0000000000000 --- a/x-pack/plugins/elastic_assistant/server/knowledge_base/esql/documentation/functions/types/sqrt.asciidoc +++ /dev/null @@ -1,8 +0,0 @@ -[%header.monospaced.styled,format=dsv,separator=|] -|=== -n | result -double | double -integer | double -long | double -unsigned_long | double -|=== diff --git a/x-pack/plugins/elastic_assistant/server/knowledge_base/esql/documentation/functions/types/starts_with.asciidoc b/x-pack/plugins/elastic_assistant/server/knowledge_base/esql/documentation/functions/types/starts_with.asciidoc deleted file mode 100644 index 6c406b80c0cad..0000000000000 --- a/x-pack/plugins/elastic_assistant/server/knowledge_base/esql/documentation/functions/types/starts_with.asciidoc +++ /dev/null @@ -1,5 +0,0 @@ -[%header.monospaced.styled,format=dsv,separator=|] -|=== -arg1 | arg2 | result -keyword | keyword | boolean -|=== diff --git a/x-pack/plugins/elastic_assistant/server/knowledge_base/esql/documentation/functions/types/substring.asciidoc b/x-pack/plugins/elastic_assistant/server/knowledge_base/esql/documentation/functions/types/substring.asciidoc deleted file mode 100644 index 2aa96ceeb7e43..0000000000000 --- a/x-pack/plugins/elastic_assistant/server/knowledge_base/esql/documentation/functions/types/substring.asciidoc +++ /dev/null @@ -1,5 +0,0 @@ -[%header.monospaced.styled,format=dsv,separator=|] -|=== -arg1 | arg2 | arg3 | result -keyword | integer | integer | keyword -|=== diff --git a/x-pack/plugins/elastic_assistant/server/knowledge_base/esql/documentation/functions/types/tan.asciidoc b/x-pack/plugins/elastic_assistant/server/knowledge_base/esql/documentation/functions/types/tan.asciidoc deleted file mode 100644 index 1df8dd6526f18..0000000000000 --- a/x-pack/plugins/elastic_assistant/server/knowledge_base/esql/documentation/functions/types/tan.asciidoc +++ /dev/null @@ -1,8 +0,0 @@ -[%header.monospaced.styled,format=dsv,separator=|] -|=== -n | result -double | double -integer | double -long | double -unsigned_long | double -|=== diff --git a/x-pack/plugins/elastic_assistant/server/knowledge_base/esql/documentation/functions/types/tanh.asciidoc b/x-pack/plugins/elastic_assistant/server/knowledge_base/esql/documentation/functions/types/tanh.asciidoc deleted file mode 100644 index 1df8dd6526f18..0000000000000 --- a/x-pack/plugins/elastic_assistant/server/knowledge_base/esql/documentation/functions/types/tanh.asciidoc +++ /dev/null @@ -1,8 +0,0 @@ -[%header.monospaced.styled,format=dsv,separator=|] -|=== -n | result -double | double -integer | double -long | double -unsigned_long | double -|=== diff --git a/x-pack/plugins/elastic_assistant/server/knowledge_base/esql/documentation/functions/types/tau.asciidoc b/x-pack/plugins/elastic_assistant/server/knowledge_base/esql/documentation/functions/types/tau.asciidoc deleted file mode 100644 index 5854465d5fb49..0000000000000 --- a/x-pack/plugins/elastic_assistant/server/knowledge_base/esql/documentation/functions/types/tau.asciidoc +++ /dev/null @@ -1,5 +0,0 @@ -[%header.monospaced.styled,format=dsv,separator=|] -|=== -result - -|=== diff --git a/x-pack/plugins/elastic_assistant/server/knowledge_base/esql/documentation/functions/types/to_ip.asciidoc b/x-pack/plugins/elastic_assistant/server/knowledge_base/esql/documentation/functions/types/to_ip.asciidoc deleted file mode 100644 index a21bbf14d87ca..0000000000000 --- a/x-pack/plugins/elastic_assistant/server/knowledge_base/esql/documentation/functions/types/to_ip.asciidoc +++ /dev/null @@ -1,6 +0,0 @@ -[%header.monospaced.styled,format=dsv,separator=|] -|=== -arg1 | result -ip | ip -keyword | ip -|=== diff --git a/x-pack/plugins/elastic_assistant/server/knowledge_base/esql/documentation/functions/types/to_string.asciidoc b/x-pack/plugins/elastic_assistant/server/knowledge_base/esql/documentation/functions/types/to_string.asciidoc deleted file mode 100644 index b8fcd4477aa70..0000000000000 --- a/x-pack/plugins/elastic_assistant/server/knowledge_base/esql/documentation/functions/types/to_string.asciidoc +++ /dev/null @@ -1,14 +0,0 @@ -[%header.monospaced.styled,format=dsv,separator=|] -|=== -v | result -boolean | keyword -datetime | keyword -double | keyword -integer | keyword -ip | keyword -keyword | keyword -long | keyword -text | keyword -unsigned_long | keyword -version | keyword -|=== diff --git a/x-pack/plugins/elastic_assistant/server/knowledge_base/esql/documentation/functions/types/to_version.asciidoc b/x-pack/plugins/elastic_assistant/server/knowledge_base/esql/documentation/functions/types/to_version.asciidoc deleted file mode 100644 index ebb83f03a6fe6..0000000000000 --- a/x-pack/plugins/elastic_assistant/server/knowledge_base/esql/documentation/functions/types/to_version.asciidoc +++ /dev/null @@ -1,7 +0,0 @@ -[%header.monospaced.styled,format=dsv,separator=|] -|=== -v | result -keyword | version -text | version -version | version -|=== diff --git a/x-pack/plugins/elastic_assistant/server/knowledge_base/esql/documentation/functions/types/trim.asciidoc b/x-pack/plugins/elastic_assistant/server/knowledge_base/esql/documentation/functions/types/trim.asciidoc deleted file mode 100644 index 11c02c8f0c3bb..0000000000000 --- a/x-pack/plugins/elastic_assistant/server/knowledge_base/esql/documentation/functions/types/trim.asciidoc +++ /dev/null @@ -1,6 +0,0 @@ -[%header.monospaced.styled,format=dsv,separator=|] -|=== -arg1 | result -keyword | keyword -text | text -|=== diff --git a/x-pack/plugins/elastic_assistant/server/knowledge_base/esql/documentation/index.asciidoc b/x-pack/plugins/elastic_assistant/server/knowledge_base/esql/documentation/index.asciidoc deleted file mode 100644 index 09b74740a5b67..0000000000000 --- a/x-pack/plugins/elastic_assistant/server/knowledge_base/esql/documentation/index.asciidoc +++ /dev/null @@ -1,71 +0,0 @@ -[[esql]] -= {esql} - -:esql-tests: {xes-repo-dir}/../../plugin/esql/qa -:esql-specs: {esql-tests}/testFixtures/src/main/resources - -[partintro] - -preview::[] - -The {es} Query Language ({esql}) provides a powerful way to filter, transform, and analyze data stored in {es}. -Users can author {esql} queries to find specific events, perform statistical analysis, and generate visualizations. -It supports a wide range of commands and functions that enable users to perform various data operations, -such as filtering, aggregation, time-series analysis, and more. - -The {es} Query Language ({esql}) makes use of "pipes" to manipulate and transform data in a step-by-step fashion. -This approach allows users to compose a series of operations, where the output of one operation becomes the input for the next, -enabling complex data transformations and analysis. - -A simple example of an {esql} query is shown below: -[source,esql] ----- -FROM employees -| EVAL age = DATE_DIFF(NOW(), birth_date, 'Y') -| STATS AVG(age) BY department -| SORT age DESC ----- - -Each {esql} query starts with a <>. A source command produces -a table, typically with data from {es}. - -image::images/esql/source-command.svg[A source command producing a table from {es},align="center"] - -A source command can be followed by one or more -<>. Processing commands change an -input table by adding, removing, or changing rows and columns. -Processing commands can perform filtering, projection, aggregation, and more. - -image::images/esql/processing-command.svg[A processing command changing an input table,align="center"] - -You can chain processing commands, separated by a pipe character: `|`. Each -processing command works on the output table of the previous command. - -image::images/esql/chaining-processing-commands.svg[Processing commands can be chained,align="center"] - -The result of a query is the table produced by the final processing command. - -[discrete] -=== The {esql} Compute Engine - -{esql} is more than a language. It represents a significant investment in new compute capabilities within {es}. -To achieve both the functional and performance requirements for {esql}, it was necessary to build an entirely new -compute architecture. {esql} search, aggregation, and transformation functions are directly executed within Elasticsearch -itself. Query expressions are not transpiled to Query DSL for execution. This approach allows {esql} to be extremely performant and versatile. - -The new {esql} execution engine was designed with performance in mind — it operates on blocks at a time instead of per row, targets vectorization and cache locality, and embraces specialization and multi-threading. It is a separate component from the existing Elasticsearch aggregation framework with different performance characteristics. - -include::esql-get-started.asciidoc[] - -include::esql-language.asciidoc[] - -include::esql-rest.asciidoc[] - -include::esql-kibana.asciidoc[] - -include::task-management.asciidoc[] - -include::esql-limitations.asciidoc[] - -:esql-tests!: -:esql-specs!: diff --git a/x-pack/plugins/elastic_assistant/server/knowledge_base/esql/documentation/metadata_fields.asciidoc b/x-pack/plugins/elastic_assistant/server/knowledge_base/esql/documentation/metadata_fields.asciidoc deleted file mode 100644 index c034d4d0dd2b3..0000000000000 --- a/x-pack/plugins/elastic_assistant/server/knowledge_base/esql/documentation/metadata_fields.asciidoc +++ /dev/null @@ -1,55 +0,0 @@ -[[esql-metadata-fields]] -=== {esql} metadata fields - -++++ -Metadata fields -++++ - -{esql} can access <>. The currently -supported ones are: - - * <>: the index to which the document belongs. - The field is of the type <>. - - * <>: the source document's ID. The field is of the - type <>. - - * `_version`: the source document's version. The field is of the type - <>. - -To enable the access to these fields, the <> source command needs -to be provided with a dedicated directive: - -[source,esql] ----- -FROM index [METADATA _index, _id] ----- - -Metadata fields are only available if the source of the data is an index. -Consequently, `FROM` is the only source commands that supports the `METADATA` -directive. - -Once enabled, the fields are then available to subsequent processing commands, just -like the other index fields: - -[source.merge.styled,esql] ----- -include::{esql-specs}/metadata-ignoreCsvTests.csv-spec[tag=multipleIndices] ----- -[%header.monospaced.styled,format=dsv,separator=|] -|=== -include::{esql-specs}/metadata-ignoreCsvTests.csv-spec[tag=multipleIndices-result] -|=== - -Also, similar to the index fields, once an aggregation is performed, a -metadata field will no longer be accessible to subsequent commands, unless -used as grouping field: - -[source.merge.styled,esql] ----- -include::{esql-specs}/metadata-ignoreCsvTests.csv-spec[tag=metaIndexInAggs] ----- -[%header.monospaced.styled,format=dsv,separator=|] -|=== -include::{esql-specs}/metadata-ignoreCsvTests.csv-spec[tag=metaIndexInAggs-result] -|=== diff --git a/x-pack/plugins/elastic_assistant/server/knowledge_base/esql/documentation/multivalued_fields.asciidoc b/x-pack/plugins/elastic_assistant/server/knowledge_base/esql/documentation/multivalued_fields.asciidoc deleted file mode 100644 index 5e48eb4ef8af8..0000000000000 --- a/x-pack/plugins/elastic_assistant/server/knowledge_base/esql/documentation/multivalued_fields.asciidoc +++ /dev/null @@ -1,240 +0,0 @@ -[[esql-multivalued-fields]] -=== {esql} multivalued fields - -++++ -Multivalued fields -++++ - -{esql} is fine reading from multivalued fields: - -[source,console,id=esql-multivalued-fields-reorders] ----- -POST /mv/_bulk?refresh -{ "index" : {} } -{ "a": 1, "b": [2, 1] } -{ "index" : {} } -{ "a": 2, "b": 3 } - -POST /_query -{ - "query": "FROM mv | LIMIT 2" -} ----- - -Multivalued fields come back as a JSON array: - -[source,console-result] ----- -{ - "columns": [ - { "name": "a", "type": "long"}, - { "name": "b", "type": "long"} - ], - "values": [ - [1, [1, 2]], - [2, 3] - ] -} ----- - -The relative order of values in a multivalued field is undefined. They'll frequently be in -ascending order but don't rely on that. - -[discrete] -[[esql-multivalued-fields-dups]] -==== Duplicate values - -Some field types, like <> remove duplicate values on write: - -[source,console,id=esql-multivalued-fields-kwdups] ----- -PUT /mv -{ - "mappings": { - "properties": { - "b": {"type": "keyword"} - } - } -} - -POST /mv/_bulk?refresh -{ "index" : {} } -{ "a": 1, "b": ["foo", "foo", "bar"] } -{ "index" : {} } -{ "a": 2, "b": ["bar", "bar"] } - -POST /_query -{ - "query": "FROM mv | LIMIT 2" -} ----- - -And {esql} sees that removal: - -[source,console-result] ----- -{ - "columns": [ - { "name": "a", "type": "long"}, - { "name": "b", "type": "keyword"} - ], - "values": [ - [1, ["bar", "foo"]], - [2, "bar"] - ] -} ----- - -But other types, like `long` don't remove duplicates. - -[source,console,id=esql-multivalued-fields-longdups] ----- -PUT /mv -{ - "mappings": { - "properties": { - "b": {"type": "long"} - } - } -} - -POST /mv/_bulk?refresh -{ "index" : {} } -{ "a": 1, "b": [2, 2, 1] } -{ "index" : {} } -{ "a": 2, "b": [1, 1] } - -POST /_query -{ - "query": "FROM mv | LIMIT 2" -} ----- - -And {esql} also sees that: - -[source,console-result] ----- -{ - "columns": [ - { "name": "a", "type": "long"}, - { "name": "b", "type": "long"} - ], - "values": [ - [1, [1, 2, 2]], - [2, [1, 1]] - ] -} ----- - -This is all at the storage layer. If you store duplicate `long`s and then -convert them to strings the duplicates will stay: - -[source,console,id=esql-multivalued-fields-longdups-tostring] ----- -PUT /mv -{ - "mappings": { - "properties": { - "b": {"type": "long"} - } - } -} - -POST /mv/_bulk?refresh -{ "index" : {} } -{ "a": 1, "b": [2, 2, 1] } -{ "index" : {} } -{ "a": 2, "b": [1, 1] } - -POST /_query -{ - "query": "FROM mv | EVAL b=TO_STRING(b) | LIMIT 2" -} ----- - -[source,console-result] ----- -{ - "columns": [ - { "name": "a", "type": "long"}, - { "name": "b", "type": "keyword"} - ], - "values": [ - [1, ["1", "2", "2"]], - [2, ["1", "1"]] - ] -} ----- - -[discrete] -[[esql-multivalued-fields-functions]] -==== Functions - -Unless otherwise documented functions will return `null` when applied to a multivalued -field. This behavior may change in a later version. - -[source,console,id=esql-multivalued-fields-mv-into-null] ----- -POST /mv/_bulk?refresh -{ "index" : {} } -{ "a": 1, "b": [2, 1] } -{ "index" : {} } -{ "a": 2, "b": 3 } - -POST /_query -{ - "query": "FROM mv | EVAL b + 2, a + b | LIMIT 4" -} ----- - -[source,console-result] ----- -{ - "columns": [ - { "name": "a", "type": "long"}, - { "name": "b", "type": "long"}, - { "name": "b+2", "type": "long"}, - { "name": "a+b", "type": "long"} - ], - "values": [ - [1, [1, 2], null, null], - [2, 3, 5, 5] - ] -} ----- - -Work around this limitation by converting the field to single value with one of: - -* <> -* <> -* <> -* <> -* <> -* <> -* <> - -[source,console,esql-multivalued-fields-mv-into-null] ----- -POST /_query -{ - "query": "FROM mv | EVAL b=MV_MIN(b) | EVAL b + 2, a + b | LIMIT 4" -} ----- -// TEST[continued] - -[source,console-result] ----- -{ - "columns": [ - { "name": "a", "type": "long"}, - { "name": "b", "type": "long"}, - { "name": "b+2", "type": "long"}, - { "name": "a+b", "type": "long"} - ], - "values": [ - [1, 1, 3, 2], - [2, 3, 5, 5] - ] -} ----- - diff --git a/x-pack/plugins/elastic_assistant/server/knowledge_base/esql/documentation/processing_commands/dissect.asciidoc b/x-pack/plugins/elastic_assistant/server/knowledge_base/esql/documentation/processing_commands/dissect.asciidoc deleted file mode 100644 index e6206615342f7..0000000000000 --- a/x-pack/plugins/elastic_assistant/server/knowledge_base/esql/documentation/processing_commands/dissect.asciidoc +++ /dev/null @@ -1,19 +0,0 @@ -[discrete] -[[esql-dissect]] -=== `DISSECT` - -`DISSECT` enables you to extract structured data out of a string. `DISSECT` -matches the string against a delimiter-based pattern, and extracts the specified -keys as columns. - -Refer to the <> for the -syntax of dissect patterns. - -[source.merge.styled,esql] ----- -include::{esql-specs}/dissect.csv-spec[tag=dissect] ----- -[%header.monospaced.styled,format=dsv,separator=|] -|=== -include::{esql-specs}/dissect.csv-spec[tag=dissect-result] -|=== diff --git a/x-pack/plugins/elastic_assistant/server/knowledge_base/esql/documentation/processing_commands/drop.asciidoc b/x-pack/plugins/elastic_assistant/server/knowledge_base/esql/documentation/processing_commands/drop.asciidoc deleted file mode 100644 index 50e3b27fb1b28..0000000000000 --- a/x-pack/plugins/elastic_assistant/server/knowledge_base/esql/documentation/processing_commands/drop.asciidoc +++ /dev/null @@ -1,18 +0,0 @@ -[discrete] -[[esql-drop]] -=== `DROP` - -Use `DROP` to remove columns: - -[source,esql] ----- -include::{esql-specs}/docs.csv-spec[tag=dropheight] ----- - -Rather than specify each column by name, you can use wildcards to drop all -columns with a name that matches a pattern: - -[source,esql] ----- -include::{esql-specs}/docs.csv-spec[tag=dropheightwithwildcard] ----- diff --git a/x-pack/plugins/elastic_assistant/server/knowledge_base/esql/documentation/processing_commands/enrich.asciidoc b/x-pack/plugins/elastic_assistant/server/knowledge_base/esql/documentation/processing_commands/enrich.asciidoc deleted file mode 100644 index 1e489119d4ca3..0000000000000 --- a/x-pack/plugins/elastic_assistant/server/knowledge_base/esql/documentation/processing_commands/enrich.asciidoc +++ /dev/null @@ -1,101 +0,0 @@ -[discrete] -[[esql-enrich]] -=== `ENRICH` - -**Syntax** - -[source,txt] ----- -ENRICH policy [ON match_field] [WITH [new_name1 = ]field1, [new_name2 = ]field2, ...] ----- - -*Parameters* - -`policy`:: -The name of the enrich policy. You need to <> -and <> the enrich policy first. - -`ON match_field`:: -The match field. `ENRICH` uses its value to look for records in the enrich -index. If not specified, the match will be performed on the column with the same -name as the `match_field` defined in the <>. - -`WITH fieldX`:: -The enrich fields from the enrich index that are added to the result as new -columns. If a column with the same name as the enrich field already exists, the -existing column will be replaced by the new column. If not specified, each of -the enrich fields defined in the policy is added - -`new_nameX =`:: -Enables you to change the name of the column that's added for each of the enrich -fields. Defaults to the enrich field name. - -*Description* - -`ENRICH` enables you to add data from existing indices as new columns using an -enrich policy. Refer to <> for information about setting up a -policy. - -image::images/esql/esql-enrich.png[align="center"] - -TIP: Before you can use `ENRICH`, you need to <>. - -*Examples* - -// tag::examples[] -The following example uses the `languages_policy` enrich policy to add a new -column for each enrich field defined in the policy. The match is performed using -the `match_field` defined in the <> and -requires that the input table has a column with the same name (`language_code` -in this example). `ENRICH` will look for records in the -<> based on the match field value. - -[source.merge.styled,esql] ----- -include::{esql-specs}/docs-ignoreCsvTests.csv-spec[tag=enrich] ----- -[%header.monospaced.styled,format=dsv,separator=|] -|=== -include::{esql-specs}/docs-ignoreCsvTests.csv-spec[tag=enrich-result] -|=== - -To use a column with a different name than the `match_field` defined in the -policy as the match field, use `ON `: - -[source.merge.styled,esql] ----- -include::{esql-specs}/docs-ignoreCsvTests.csv-spec[tag=enrich_on] ----- -[%header.monospaced.styled,format=dsv,separator=|] -|=== -include::{esql-specs}/docs-ignoreCsvTests.csv-spec[tag=enrich_on-result] -|=== - -By default, each of the enrich fields defined in the policy is added as a -column. To explicitly select the enrich fields that are added, use -`WITH , ...`: - -[source.merge.styled,esql] ----- -include::{esql-specs}/docs-ignoreCsvTests.csv-spec[tag=enrich_with] ----- -[%header.monospaced.styled,format=dsv,separator=|] -|=== -include::{esql-specs}/docs-ignoreCsvTests.csv-spec[tag=enrich_with-result] -|=== - -You can rename the columns that are added using `WITH new_name=`: - -[source.merge.styled,esql] ----- -include::{esql-specs}/docs-ignoreCsvTests.csv-spec[tag=enrich_rename] ----- -[%header.monospaced.styled,format=dsv,separator=|] -|=== -include::{esql-specs}/docs-ignoreCsvTests.csv-spec[tag=enrich_rename-result] -|=== - -In case of name collisions, the newly created columns will override existing -columns. -// end::examples[] diff --git a/x-pack/plugins/elastic_assistant/server/knowledge_base/esql/documentation/processing_commands/eval.asciidoc b/x-pack/plugins/elastic_assistant/server/knowledge_base/esql/documentation/processing_commands/eval.asciidoc deleted file mode 100644 index a0a78f2a3bf97..0000000000000 --- a/x-pack/plugins/elastic_assistant/server/knowledge_base/esql/documentation/processing_commands/eval.asciidoc +++ /dev/null @@ -1,30 +0,0 @@ -[discrete] -[[esql-eval]] -=== `EVAL` -`EVAL` enables you to append new columns: - -[source.merge.styled,esql] ----- -include::{esql-specs}/docs.csv-spec[tag=eval] ----- -[%header.monospaced.styled,format=dsv,separator=|] -|=== -include::{esql-specs}/docs.csv-spec[tag=eval-result] -|=== - -If the specified column already exists, the existing column will be dropped, and -the new column will be appended to the table: - -[source.merge.styled,esql] ----- -include::{esql-specs}/docs.csv-spec[tag=evalReplace] ----- -[%header.monospaced.styled,format=dsv,separator=|] -|=== -include::{esql-specs}/docs.csv-spec[tag=evalReplace-result] -|=== - -[discrete] -==== Functions -`EVAL` supports various functions for calculating values. Refer to -<> for more information. diff --git a/x-pack/plugins/elastic_assistant/server/knowledge_base/esql/documentation/processing_commands/grok.asciidoc b/x-pack/plugins/elastic_assistant/server/knowledge_base/esql/documentation/processing_commands/grok.asciidoc deleted file mode 100644 index 914c13b2320eb..0000000000000 --- a/x-pack/plugins/elastic_assistant/server/knowledge_base/esql/documentation/processing_commands/grok.asciidoc +++ /dev/null @@ -1,21 +0,0 @@ -[discrete] -[[esql-grok]] -=== `GROK` - -`GROK` enables you to extract structured data out of a string. `GROK` matches -the string against patterns, based on regular expressions, and extracts the -specified patterns as columns. - -Refer to the <> for the syntax for -of grok patterns. - -For example: - -[source.merge.styled,esql] ----- -include::{esql-specs}/grok.csv-spec[tag=grok] ----- -[%header.monospaced.styled,format=dsv,separator=|] -|=== -include::{esql-specs}/grok.csv-spec[tag=grok-result] -|=== diff --git a/x-pack/plugins/elastic_assistant/server/knowledge_base/esql/documentation/processing_commands/keep.asciidoc b/x-pack/plugins/elastic_assistant/server/knowledge_base/esql/documentation/processing_commands/keep.asciidoc deleted file mode 100644 index 3e54e5a7d1c5c..0000000000000 --- a/x-pack/plugins/elastic_assistant/server/knowledge_base/esql/documentation/processing_commands/keep.asciidoc +++ /dev/null @@ -1,35 +0,0 @@ -[discrete] -[[esql-keep]] -=== `KEEP` - -The `KEEP` command enables you to specify what columns are returned and the -order in which they are returned. - -To limit the columns that are returned, use a comma-separated list of column -names. The columns are returned in the specified order: - -[source.merge.styled,esql] ----- -include::{esql-specs}/docs.csv-spec[tag=keep] ----- -[%header.monospaced.styled,format=dsv,separator=|] -|=== -include::{esql-specs}/docs.csv-spec[tag=keep-result] -|=== - -Rather than specify each column by name, you can use wildcards to return all -columns with a name that matches a pattern: - -[source,esql] ----- -include::{esql-specs}/docs.csv-spec[tag=keepWildcard] ----- - -The asterisk wildcard (`*`) by itself translates to all columns that do not -match the other arguments. This query will first return all columns with a name -that starts with an h, followed by all other columns: - -[source,esql] ----- -include::{esql-specs}/docs.csv-spec[tag=keepDoubleWildcard] ----- diff --git a/x-pack/plugins/elastic_assistant/server/knowledge_base/esql/documentation/processing_commands/limit.asciidoc b/x-pack/plugins/elastic_assistant/server/knowledge_base/esql/documentation/processing_commands/limit.asciidoc deleted file mode 100644 index c02b534af59e1..0000000000000 --- a/x-pack/plugins/elastic_assistant/server/knowledge_base/esql/documentation/processing_commands/limit.asciidoc +++ /dev/null @@ -1,13 +0,0 @@ -[discrete] -[[esql-limit]] -=== `LIMIT` - -The `LIMIT` processing command enables you to limit the number of rows: - -[source,esql] ----- -include::{esql-specs}/docs.csv-spec[tag=limit] ----- - -If not specified, `LIMIT` defaults to `500`. A single query will not return -more than 10,000 rows, regardless of the `LIMIT` value. diff --git a/x-pack/plugins/elastic_assistant/server/knowledge_base/esql/documentation/processing_commands/mv_expand.asciidoc b/x-pack/plugins/elastic_assistant/server/knowledge_base/esql/documentation/processing_commands/mv_expand.asciidoc deleted file mode 100644 index d62b28aabe440..0000000000000 --- a/x-pack/plugins/elastic_assistant/server/knowledge_base/esql/documentation/processing_commands/mv_expand.asciidoc +++ /dev/null @@ -1,14 +0,0 @@ -[discrete] -[[esql-mv_expand]] -=== `MV_EXPAND` - -The `MV_EXPAND` processing command expands multivalued fields into one row per value, duplicating other fields: - -[source.merge.styled,esql] ----- -include::{esql-specs}/mv_expand.csv-spec[tag=simple] ----- -[%header.monospaced.styled,format=dsv,separator=|] -|=== -include::{esql-specs}/mv_expand.csv-spec[tag=simple-result] -|=== diff --git a/x-pack/plugins/elastic_assistant/server/knowledge_base/esql/documentation/processing_commands/rename.asciidoc b/x-pack/plugins/elastic_assistant/server/knowledge_base/esql/documentation/processing_commands/rename.asciidoc deleted file mode 100644 index 1dda424317976..0000000000000 --- a/x-pack/plugins/elastic_assistant/server/knowledge_base/esql/documentation/processing_commands/rename.asciidoc +++ /dev/null @@ -1,27 +0,0 @@ -[discrete] -[[esql-rename]] -=== `RENAME` - -Use `RENAME` to rename a column using the following syntax: - -[source,esql] ----- -RENAME AS ----- - -For example: - -[source,esql] ----- -include::{esql-specs}/docs.csv-spec[tag=rename] ----- - -If a column with the new name already exists, it will be replaced by the new -column. - -Multiple columns can be renamed with a single `RENAME` command: - -[source,esql] ----- -include::{esql-specs}/docs.csv-spec[tag=renameMultipleColumns] ----- diff --git a/x-pack/plugins/elastic_assistant/server/knowledge_base/esql/documentation/processing_commands/sort.asciidoc b/x-pack/plugins/elastic_assistant/server/knowledge_base/esql/documentation/processing_commands/sort.asciidoc deleted file mode 100644 index 76a9193375932..0000000000000 --- a/x-pack/plugins/elastic_assistant/server/knowledge_base/esql/documentation/processing_commands/sort.asciidoc +++ /dev/null @@ -1,37 +0,0 @@ -[discrete] -[[esql-sort]] -=== `SORT` -Use the `SORT` command to sort rows on one or more fields: - -[source,esql] ----- -include::{esql-specs}/docs.csv-spec[tag=sort] ----- - -The default sort order is ascending. Set an explicit sort order using `ASC` or -`DESC`: - -[source,esql] ----- -include::{esql-specs}/docs.csv-spec[tag=sortDesc] ----- - -Two rows with the same sort key are considered equal. You can provide additional -sort expressions to act as tie breakers: - -[source,esql] ----- -include::{esql-specs}/docs.csv-spec[tag=sortTie] ----- - -[discrete] -==== `null` values -By default, `null` values are treated as being larger than any other value. With -an ascending sort order, `null` values are sorted last, and with a descending -sort order, `null` values are sorted first. You can change that by providing -`NULLS FIRST` or `NULLS LAST`: - -[source,esql] ----- -include::{esql-specs}/docs.csv-spec[tag=sortNullsFirst] ----- diff --git a/x-pack/plugins/elastic_assistant/server/knowledge_base/esql/documentation/processing_commands/stats.asciidoc b/x-pack/plugins/elastic_assistant/server/knowledge_base/esql/documentation/processing_commands/stats.asciidoc deleted file mode 100644 index 71f4470e3dfb0..0000000000000 --- a/x-pack/plugins/elastic_assistant/server/knowledge_base/esql/documentation/processing_commands/stats.asciidoc +++ /dev/null @@ -1,45 +0,0 @@ -[discrete] -[[esql-stats-by]] -=== `STATS ... BY` -Use `STATS ... BY` to group rows according to a common value and calculate one -or more aggregated values over the grouped rows. - -[source.merge.styled,esql] ----- -include::{esql-specs}/docs.csv-spec[tag=stats] ----- -[%header.monospaced.styled,format=dsv,separator=|] -|=== -include::{esql-specs}/docs.csv-spec[tag=stats-result] -|=== - -If `BY` is omitted, the output table contains exactly one row with the -aggregations applied over the entire dataset: - -[source.merge.styled,esql] ----- -include::{esql-specs}/docs.csv-spec[tag=statsWithoutBy] ----- -[%header.monospaced.styled,format=dsv,separator=|] -|=== -include::{esql-specs}/docs.csv-spec[tag=statsWithoutBy-result] -|=== - -It's possible to calculate multiple values: - -[source,esql] ----- -include::{esql-specs}/docs.csv-spec[tag=statsCalcMultipleValues] ----- - -It's also possible to group by multiple values (only supported for long and -keyword family fields): - -[source,esql] ----- -include::{esql-specs}/docs.csv-spec[tag=statsGroupByMultipleValues] ----- - -The following aggregation functions are supported: - -include::../functions/aggregation-functions.asciidoc[tag=agg_list] diff --git a/x-pack/plugins/elastic_assistant/server/knowledge_base/esql/documentation/processing_commands/where.asciidoc b/x-pack/plugins/elastic_assistant/server/knowledge_base/esql/documentation/processing_commands/where.asciidoc deleted file mode 100644 index 8dd55df12b9e7..0000000000000 --- a/x-pack/plugins/elastic_assistant/server/knowledge_base/esql/documentation/processing_commands/where.asciidoc +++ /dev/null @@ -1,33 +0,0 @@ -[discrete] -[[esql-where]] -=== `WHERE` - -Use `WHERE` to produce a table that contains all the rows from the input table -for which the provided condition evaluates to `true`: - -[source,esql] ----- -include::{esql-specs}/docs.csv-spec[tag=where] ----- - -Which, if `still_hired` is a boolean field, can be simplified to: - -[source,esql] ----- -include::{esql-specs}/docs.csv-spec[tag=whereBoolean] ----- - -[discrete] -==== Operators - -Refer to <> for an overview of the supported operators. - -[discrete] -==== Functions -`WHERE` supports various functions for calculating values. Refer to -<> for more information. - -[source,esql] ----- -include::{esql-specs}/docs.csv-spec[tag=whereFunction] ----- diff --git a/x-pack/plugins/elastic_assistant/server/knowledge_base/esql/documentation/source_commands/from.asciidoc b/x-pack/plugins/elastic_assistant/server/knowledge_base/esql/documentation/source_commands/from.asciidoc deleted file mode 100644 index 5718bfc27ac1c..0000000000000 --- a/x-pack/plugins/elastic_assistant/server/knowledge_base/esql/documentation/source_commands/from.asciidoc +++ /dev/null @@ -1,37 +0,0 @@ -[discrete] -[[esql-from]] -=== `FROM` - -The `FROM` source command returns a table with up to 10,000 documents from a -data stream, index, or alias. Each row in the resulting table represents a -document. Each column corresponds to a field, and can be accessed by the name -of that field. - -[source,esql] ----- -FROM employees ----- - -You can use <> to refer to indices, aliases -and data streams. This can be useful for time series data, for example to access -today's index: - -[source,esql] ----- -FROM ----- - -Use comma-separated lists or wildcards to query multiple data streams, indices, -or aliases: - -[source,esql] ----- -FROM employees-00001,other-employees-* ----- - -Use the `METADATA` directive to enable <>: - -[source,esql] ----- -FROM employees [METADATA _id] ----- diff --git a/x-pack/plugins/elastic_assistant/server/knowledge_base/esql/documentation/source_commands/row.asciidoc b/x-pack/plugins/elastic_assistant/server/knowledge_base/esql/documentation/source_commands/row.asciidoc deleted file mode 100644 index edfe5ecbf7cf3..0000000000000 --- a/x-pack/plugins/elastic_assistant/server/knowledge_base/esql/documentation/source_commands/row.asciidoc +++ /dev/null @@ -1,29 +0,0 @@ -[discrete] -[[esql-row]] -=== `ROW` - -The `ROW` source command produces a row with one or more columns with values -that you specify. This can be useful for testing. - -[source.merge.styled,esql] ----- -include::{esql-specs}/row.csv-spec[tag=example] ----- -[%header.monospaced.styled,format=dsv,separator=|] -|=== -include::{esql-specs}/row.csv-spec[tag=example-result] -|=== - -Use square brackets to create multi-value columns: - -[source,esql] ----- -include::{esql-specs}/row.csv-spec[tag=multivalue] ----- - -`ROW` supports the use of <>: - -[source,esql] ----- -include::{esql-specs}/row.csv-spec[tag=function] ----- diff --git a/x-pack/plugins/elastic_assistant/server/knowledge_base/esql/documentation/source_commands/show.asciidoc b/x-pack/plugins/elastic_assistant/server/knowledge_base/esql/documentation/source_commands/show.asciidoc deleted file mode 100644 index 956baf628e9f3..0000000000000 --- a/x-pack/plugins/elastic_assistant/server/knowledge_base/esql/documentation/source_commands/show.asciidoc +++ /dev/null @@ -1,10 +0,0 @@ -[discrete] -[[esql-show]] -=== `SHOW ` - -The `SHOW ` source command returns information about the deployment and -its capabilities: - -* Use `SHOW INFO` to return the deployment's version, build date and hash. -* Use `SHOW FUNCTIONS` to return a list of all supported functions and a -synopsis of each function. diff --git a/x-pack/plugins/elastic_assistant/server/knowledge_base/esql/documentation/task_management.asciidoc b/x-pack/plugins/elastic_assistant/server/knowledge_base/esql/documentation/task_management.asciidoc deleted file mode 100644 index 96a624c89bf7d..0000000000000 --- a/x-pack/plugins/elastic_assistant/server/knowledge_base/esql/documentation/task_management.asciidoc +++ /dev/null @@ -1,35 +0,0 @@ -[[esql-task-management]] -== {esql} task management - -++++ -Task management -++++ - -You can list running {esql} queries with the <>: - -[source,console,id=esql-task-management-get-all] ----- -GET /_tasks?pretty&detailed&group_by=parents&human&actions=*data/read/esql ----- - -Which returns a list of statuses like this: - -[source,js] ----- -include::{esql-specs}/query_task.json[] ----- -// NOTCONSOLE -// Tested in a unit test - -<1> The user submitted query. -<2> Time the query has been running. - -You can use this to find long running queries and, if you need to, cancel them -with the <>: - -[source,console,id=esql-task-management-cancelEsqlQueryRequestTests] ----- -POST _tasks/2j8UKw1bRO283PMwDugNNg:5326/_cancel ----- - -It may take a few seconds for the query to be stopped. diff --git a/x-pack/plugins/elastic_assistant/server/knowledge_base/esql/language_definition/esql_base_lexer.g4 b/x-pack/plugins/elastic_assistant/server/knowledge_base/esql/language_definition/esql_base_lexer.g4 deleted file mode 100644 index 747c1fdcd1921..0000000000000 --- a/x-pack/plugins/elastic_assistant/server/knowledge_base/esql/language_definition/esql_base_lexer.g4 +++ /dev/null @@ -1,191 +0,0 @@ -lexer grammar EsqlBaseLexer; - -DISSECT : 'dissect' -> pushMode(EXPRESSION); -DROP : 'drop' -> pushMode(SOURCE_IDENTIFIERS); -ENRICH : 'enrich' -> pushMode(SOURCE_IDENTIFIERS); -EVAL : 'eval' -> pushMode(EXPRESSION); -EXPLAIN : 'explain' -> pushMode(EXPLAIN_MODE); -FROM : 'from' -> pushMode(SOURCE_IDENTIFIERS); -GROK : 'grok' -> pushMode(EXPRESSION); -INLINESTATS : 'inlinestats' -> pushMode(EXPRESSION); -KEEP : 'keep' -> pushMode(SOURCE_IDENTIFIERS); -LIMIT : 'limit' -> pushMode(EXPRESSION); -MV_EXPAND : 'mv_expand' -> pushMode(SOURCE_IDENTIFIERS); -PROJECT : 'project' -> pushMode(SOURCE_IDENTIFIERS); -RENAME : 'rename' -> pushMode(SOURCE_IDENTIFIERS); -ROW : 'row' -> pushMode(EXPRESSION); -SHOW : 'show' -> pushMode(EXPRESSION); -SORT : 'sort' -> pushMode(EXPRESSION); -STATS : 'stats' -> pushMode(EXPRESSION); -WHERE : 'where' -> pushMode(EXPRESSION); -UNKNOWN_CMD : ~[ \r\n\t[\]/]+ -> pushMode(EXPRESSION); - -LINE_COMMENT - : '//' ~[\r\n]* '\r'? '\n'? -> channel(HIDDEN) - ; - -MULTILINE_COMMENT - : '/*' (MULTILINE_COMMENT|.)*? '*/' -> channel(HIDDEN) - ; - -WS - : [ \r\n\t]+ -> channel(HIDDEN) - ; - - -mode EXPLAIN_MODE; -EXPLAIN_OPENING_BRACKET : '[' -> type(OPENING_BRACKET), pushMode(DEFAULT_MODE); -EXPLAIN_PIPE : '|' -> type(PIPE), popMode; -EXPLAIN_WS : WS -> channel(HIDDEN); -EXPLAIN_LINE_COMMENT : LINE_COMMENT -> channel(HIDDEN); -EXPLAIN_MULTILINE_COMMENT : MULTILINE_COMMENT -> channel(HIDDEN); - -mode EXPRESSION; - -PIPE : '|' -> popMode; - -fragment DIGIT - : [0-9] - ; - -fragment LETTER - : [A-Za-z] - ; - -fragment ESCAPE_SEQUENCE - : '\\' [tnr"\\] - ; - -fragment UNESCAPED_CHARS - : ~[\r\n"\\] - ; - -fragment EXPONENT - : [Ee] [+-]? DIGIT+ - ; - -STRING - : '"' (ESCAPE_SEQUENCE | UNESCAPED_CHARS)* '"' - | '"""' (~[\r\n])*? '"""' '"'? '"'? - ; - -INTEGER_LITERAL - : DIGIT+ - ; - -DECIMAL_LITERAL - : DIGIT+ DOT DIGIT* - | DOT DIGIT+ - | DIGIT+ (DOT DIGIT*)? EXPONENT - | DOT DIGIT+ EXPONENT - ; - -BY : 'by'; - -AND : 'and'; -ASC : 'asc'; -ASSIGN : '='; -COMMA : ','; -DESC : 'desc'; -DOT : '.'; -FALSE : 'false'; -FIRST : 'first'; -LAST : 'last'; -LP : '('; -IN: 'in'; -IS: 'is'; -LIKE: 'like'; -NOT : 'not'; -NULL : 'null'; -NULLS : 'nulls'; -OR : 'or'; -PARAM: '?'; -RLIKE: 'rlike'; -RP : ')'; -TRUE : 'true'; -INFO : 'info'; -FUNCTIONS : 'functions'; - -EQ : '=='; -NEQ : '!='; -LT : '<'; -LTE : '<='; -GT : '>'; -GTE : '>='; - -PLUS : '+'; -MINUS : '-'; -ASTERISK : '*'; -SLASH : '/'; -PERCENT : '%'; - -// Brackets are funny. We can happen upon a CLOSING_BRACKET in two ways - one -// way is to start in an explain command which then shifts us to expression -// mode. Thus, the two popModes on CLOSING_BRACKET. The other way could as -// the start of a multivalued field constant. To line up with the double pop -// the explain mode needs, we double push when we see that. -OPENING_BRACKET : '[' -> pushMode(EXPRESSION), pushMode(EXPRESSION); -CLOSING_BRACKET : ']' -> popMode, popMode; - - -UNQUOTED_IDENTIFIER - : LETTER (LETTER | DIGIT | '_')* - // only allow @ at beginning of identifier to keep the option to allow @ as infix operator in the future - // also, single `_` and `@` characters are not valid identifiers - | ('_' | '@') (LETTER | DIGIT | '_')+ - ; - -QUOTED_IDENTIFIER - : '`' ( ~'`' | '``' )* '`' - ; - -EXPR_LINE_COMMENT - : LINE_COMMENT -> channel(HIDDEN) - ; - -EXPR_MULTILINE_COMMENT - : MULTILINE_COMMENT -> channel(HIDDEN) - ; - -EXPR_WS - : WS -> channel(HIDDEN) - ; - - - -mode SOURCE_IDENTIFIERS; - -SRC_PIPE : '|' -> type(PIPE), popMode; -SRC_OPENING_BRACKET : '[' -> type(OPENING_BRACKET), pushMode(SOURCE_IDENTIFIERS), pushMode(SOURCE_IDENTIFIERS); -SRC_CLOSING_BRACKET : ']' -> popMode, popMode, type(CLOSING_BRACKET); -SRC_COMMA : ',' -> type(COMMA); -SRC_ASSIGN : '=' -> type(ASSIGN); -AS : 'as'; -METADATA: 'metadata'; -ON : 'on'; -WITH : 'with'; - -SRC_UNQUOTED_IDENTIFIER - : SRC_UNQUOTED_IDENTIFIER_PART+ - ; - -fragment SRC_UNQUOTED_IDENTIFIER_PART - : ~[=`|,[\]/ \t\r\n]+ - | '/' ~[*/] // allow single / but not followed by another / or * which would start a comment - ; - -SRC_QUOTED_IDENTIFIER - : QUOTED_IDENTIFIER - ; - -SRC_LINE_COMMENT - : LINE_COMMENT -> channel(HIDDEN) - ; - -SRC_MULTILINE_COMMENT - : MULTILINE_COMMENT -> channel(HIDDEN) - ; - -SRC_WS - : WS -> channel(HIDDEN) - ; diff --git a/x-pack/plugins/elastic_assistant/server/knowledge_base/esql/language_definition/esql_base_lexer.tokens b/x-pack/plugins/elastic_assistant/server/knowledge_base/esql/language_definition/esql_base_lexer.tokens deleted file mode 100644 index d8761f5eb0d73..0000000000000 --- a/x-pack/plugins/elastic_assistant/server/knowledge_base/esql/language_definition/esql_base_lexer.tokens +++ /dev/null @@ -1,137 +0,0 @@ -DISSECT=1 -DROP=2 -ENRICH=3 -EVAL=4 -EXPLAIN=5 -FROM=6 -GROK=7 -INLINESTATS=8 -KEEP=9 -LIMIT=10 -MV_EXPAND=11 -PROJECT=12 -RENAME=13 -ROW=14 -SHOW=15 -SORT=16 -STATS=17 -WHERE=18 -UNKNOWN_CMD=19 -LINE_COMMENT=20 -MULTILINE_COMMENT=21 -WS=22 -EXPLAIN_WS=23 -EXPLAIN_LINE_COMMENT=24 -EXPLAIN_MULTILINE_COMMENT=25 -PIPE=26 -STRING=27 -INTEGER_LITERAL=28 -DECIMAL_LITERAL=29 -BY=30 -AND=31 -ASC=32 -ASSIGN=33 -COMMA=34 -DESC=35 -DOT=36 -FALSE=37 -FIRST=38 -LAST=39 -LP=40 -IN=41 -IS=42 -LIKE=43 -NOT=44 -NULL=45 -NULLS=46 -OR=47 -PARAM=48 -RLIKE=49 -RP=50 -TRUE=51 -INFO=52 -FUNCTIONS=53 -EQ=54 -NEQ=55 -LT=56 -LTE=57 -GT=58 -GTE=59 -PLUS=60 -MINUS=61 -ASTERISK=62 -SLASH=63 -PERCENT=64 -OPENING_BRACKET=65 -CLOSING_BRACKET=66 -UNQUOTED_IDENTIFIER=67 -QUOTED_IDENTIFIER=68 -EXPR_LINE_COMMENT=69 -EXPR_MULTILINE_COMMENT=70 -EXPR_WS=71 -AS=72 -METADATA=73 -ON=74 -WITH=75 -SRC_UNQUOTED_IDENTIFIER=76 -SRC_QUOTED_IDENTIFIER=77 -SRC_LINE_COMMENT=78 -SRC_MULTILINE_COMMENT=79 -SRC_WS=80 -EXPLAIN_PIPE=81 -'dissect'=1 -'drop'=2 -'enrich'=3 -'eval'=4 -'explain'=5 -'from'=6 -'grok'=7 -'inlinestats'=8 -'keep'=9 -'limit'=10 -'mv_expand'=11 -'project'=12 -'rename'=13 -'row'=14 -'show'=15 -'sort'=16 -'stats'=17 -'where'=18 -'by'=30 -'and'=31 -'asc'=32 -'desc'=35 -'.'=36 -'false'=37 -'first'=38 -'last'=39 -'('=40 -'in'=41 -'is'=42 -'like'=43 -'not'=44 -'null'=45 -'nulls'=46 -'or'=47 -'?'=48 -'rlike'=49 -')'=50 -'true'=51 -'info'=52 -'functions'=53 -'=='=54 -'!='=55 -'<'=56 -'<='=57 -'>'=58 -'>='=59 -'+'=60 -'-'=61 -'*'=62 -'/'=63 -'%'=64 -']'=66 -'as'=72 -'metadata'=73 -'on'=74 -'with'=75 diff --git a/x-pack/plugins/elastic_assistant/server/knowledge_base/esql/language_definition/esql_base_parser.g4 b/x-pack/plugins/elastic_assistant/server/knowledge_base/esql/language_definition/esql_base_parser.g4 deleted file mode 100644 index 044e920744375..0000000000000 --- a/x-pack/plugins/elastic_assistant/server/knowledge_base/esql/language_definition/esql_base_parser.g4 +++ /dev/null @@ -1,246 +0,0 @@ - -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -parser grammar EsqlBaseParser; - -options {tokenVocab=EsqlBaseLexer;} - -singleStatement - : query EOF - ; - -query - : sourceCommand #singleCommandQuery - | query PIPE processingCommand #compositeQuery - ; - -sourceCommand - : explainCommand - | fromCommand - | rowCommand - | showCommand - ; - -processingCommand - : evalCommand - | inlinestatsCommand - | limitCommand - | keepCommand - | sortCommand - | statsCommand - | whereCommand - | dropCommand - | renameCommand - | dissectCommand - | grokCommand - | enrichCommand - | mvExpandCommand - ; - -whereCommand - : WHERE booleanExpression - ; - -booleanExpression - : NOT booleanExpression #logicalNot - | valueExpression #booleanDefault - | regexBooleanExpression #regexExpression - | left=booleanExpression operator=AND right=booleanExpression #logicalBinary - | left=booleanExpression operator=OR right=booleanExpression #logicalBinary - | valueExpression (NOT)? IN LP valueExpression (COMMA valueExpression)* RP #logicalIn - | valueExpression IS NOT? NULL #isNull - ; - -regexBooleanExpression - : valueExpression (NOT)? kind=LIKE pattern=string - | valueExpression (NOT)? kind=RLIKE pattern=string - ; - -valueExpression - : operatorExpression #valueExpressionDefault - | left=operatorExpression comparisonOperator right=operatorExpression #comparison - ; - -operatorExpression - : primaryExpression #operatorExpressionDefault - | operator=(MINUS | PLUS) operatorExpression #arithmeticUnary - | left=operatorExpression operator=(ASTERISK | SLASH | PERCENT) right=operatorExpression #arithmeticBinary - | left=operatorExpression operator=(PLUS | MINUS) right=operatorExpression #arithmeticBinary - ; - -primaryExpression - : constant #constantDefault - | qualifiedName #dereference - | functionExpression #function - | LP booleanExpression RP #parenthesizedExpression - ; - -functionExpression - : identifier LP (ASTERISK | (booleanExpression (COMMA booleanExpression)*))? RP - ; - -rowCommand - : ROW fields - ; - -fields - : field (COMMA field)* - ; - -field - : booleanExpression - | qualifiedName ASSIGN booleanExpression - ; - -fromCommand - : FROM sourceIdentifier (COMMA sourceIdentifier)* metadata? - ; - -metadata - : OPENING_BRACKET METADATA sourceIdentifier (COMMA sourceIdentifier)* CLOSING_BRACKET - ; - - -evalCommand - : EVAL fields - ; - -statsCommand - : STATS fields? (BY grouping)? - ; - -inlinestatsCommand - : INLINESTATS fields (BY grouping)? - ; - -grouping - : qualifiedName (COMMA qualifiedName)* - ; - -sourceIdentifier - : SRC_UNQUOTED_IDENTIFIER - | SRC_QUOTED_IDENTIFIER - ; - -qualifiedName - : identifier (DOT identifier)* - ; - - -identifier - : UNQUOTED_IDENTIFIER - | QUOTED_IDENTIFIER - ; - -constant - : NULL #nullLiteral - | integerValue UNQUOTED_IDENTIFIER #qualifiedIntegerLiteral - | decimalValue #decimalLiteral - | integerValue #integerLiteral - | booleanValue #booleanLiteral - | PARAM #inputParam - | string #stringLiteral - | OPENING_BRACKET numericValue (COMMA numericValue)* CLOSING_BRACKET #numericArrayLiteral - | OPENING_BRACKET booleanValue (COMMA booleanValue)* CLOSING_BRACKET #booleanArrayLiteral - | OPENING_BRACKET string (COMMA string)* CLOSING_BRACKET #stringArrayLiteral - ; - -limitCommand - : LIMIT INTEGER_LITERAL - ; - -sortCommand - : SORT orderExpression (COMMA orderExpression)* - ; - -orderExpression - : booleanExpression ordering=(ASC | DESC)? (NULLS nullOrdering=(FIRST | LAST))? - ; - -keepCommand - : KEEP sourceIdentifier (COMMA sourceIdentifier)* - | PROJECT sourceIdentifier (COMMA sourceIdentifier)* - ; - -dropCommand - : DROP sourceIdentifier (COMMA sourceIdentifier)* - ; - -renameCommand - : RENAME renameClause (COMMA renameClause)* - ; - -renameClause: - oldName=sourceIdentifier AS newName=sourceIdentifier - ; - -dissectCommand - : DISSECT primaryExpression string commandOptions? - ; - -grokCommand - : GROK primaryExpression string - ; - -mvExpandCommand - : MV_EXPAND sourceIdentifier - ; - -commandOptions - : commandOption (COMMA commandOption)* - ; - -commandOption - : identifier ASSIGN constant - ; - -booleanValue - : TRUE | FALSE - ; - -numericValue - : decimalValue - | integerValue - ; - -decimalValue - : (PLUS | MINUS)? DECIMAL_LITERAL - ; - -integerValue - : (PLUS | MINUS)? INTEGER_LITERAL - ; - -string - : STRING - ; - -comparisonOperator - : EQ | NEQ | LT | LTE | GT | GTE - ; - -explainCommand - : EXPLAIN subqueryExpression - ; - -subqueryExpression - : OPENING_BRACKET query CLOSING_BRACKET - ; - -showCommand - : SHOW INFO #showInfo - | SHOW FUNCTIONS #showFunctions - ; - -enrichCommand - : ENRICH policyName=sourceIdentifier (ON matchField=sourceIdentifier)? (WITH enrichWithClause (COMMA enrichWithClause)*)? - ; - -enrichWithClause - : (newName=sourceIdentifier ASSIGN)? enrichField=sourceIdentifier - ; diff --git a/x-pack/plugins/elastic_assistant/server/knowledge_base/esql/language_definition/esql_base_parser.tokens b/x-pack/plugins/elastic_assistant/server/knowledge_base/esql/language_definition/esql_base_parser.tokens deleted file mode 100644 index d8761f5eb0d73..0000000000000 --- a/x-pack/plugins/elastic_assistant/server/knowledge_base/esql/language_definition/esql_base_parser.tokens +++ /dev/null @@ -1,137 +0,0 @@ -DISSECT=1 -DROP=2 -ENRICH=3 -EVAL=4 -EXPLAIN=5 -FROM=6 -GROK=7 -INLINESTATS=8 -KEEP=9 -LIMIT=10 -MV_EXPAND=11 -PROJECT=12 -RENAME=13 -ROW=14 -SHOW=15 -SORT=16 -STATS=17 -WHERE=18 -UNKNOWN_CMD=19 -LINE_COMMENT=20 -MULTILINE_COMMENT=21 -WS=22 -EXPLAIN_WS=23 -EXPLAIN_LINE_COMMENT=24 -EXPLAIN_MULTILINE_COMMENT=25 -PIPE=26 -STRING=27 -INTEGER_LITERAL=28 -DECIMAL_LITERAL=29 -BY=30 -AND=31 -ASC=32 -ASSIGN=33 -COMMA=34 -DESC=35 -DOT=36 -FALSE=37 -FIRST=38 -LAST=39 -LP=40 -IN=41 -IS=42 -LIKE=43 -NOT=44 -NULL=45 -NULLS=46 -OR=47 -PARAM=48 -RLIKE=49 -RP=50 -TRUE=51 -INFO=52 -FUNCTIONS=53 -EQ=54 -NEQ=55 -LT=56 -LTE=57 -GT=58 -GTE=59 -PLUS=60 -MINUS=61 -ASTERISK=62 -SLASH=63 -PERCENT=64 -OPENING_BRACKET=65 -CLOSING_BRACKET=66 -UNQUOTED_IDENTIFIER=67 -QUOTED_IDENTIFIER=68 -EXPR_LINE_COMMENT=69 -EXPR_MULTILINE_COMMENT=70 -EXPR_WS=71 -AS=72 -METADATA=73 -ON=74 -WITH=75 -SRC_UNQUOTED_IDENTIFIER=76 -SRC_QUOTED_IDENTIFIER=77 -SRC_LINE_COMMENT=78 -SRC_MULTILINE_COMMENT=79 -SRC_WS=80 -EXPLAIN_PIPE=81 -'dissect'=1 -'drop'=2 -'enrich'=3 -'eval'=4 -'explain'=5 -'from'=6 -'grok'=7 -'inlinestats'=8 -'keep'=9 -'limit'=10 -'mv_expand'=11 -'project'=12 -'rename'=13 -'row'=14 -'show'=15 -'sort'=16 -'stats'=17 -'where'=18 -'by'=30 -'and'=31 -'asc'=32 -'desc'=35 -'.'=36 -'false'=37 -'first'=38 -'last'=39 -'('=40 -'in'=41 -'is'=42 -'like'=43 -'not'=44 -'null'=45 -'nulls'=46 -'or'=47 -'?'=48 -'rlike'=49 -')'=50 -'true'=51 -'info'=52 -'functions'=53 -'=='=54 -'!='=55 -'<'=56 -'<='=57 -'>'=58 -'>='=59 -'+'=60 -'-'=61 -'*'=62 -'/'=63 -'%'=64 -']'=66 -'as'=72 -'metadata'=73 -'on'=74 -'with'=75 diff --git a/x-pack/plugins/elastic_assistant/server/lib/langchain/content_loaders/add_required_kb_resource_metadata.test.ts b/x-pack/plugins/elastic_assistant/server/lib/langchain/content_loaders/add_required_kb_resource_metadata.test.ts index c66c18cd434ad..48aa99c2e09af 100644 --- a/x-pack/plugins/elastic_assistant/server/lib/langchain/content_loaders/add_required_kb_resource_metadata.test.ts +++ b/x-pack/plugins/elastic_assistant/server/lib/langchain/content_loaders/add_required_kb_resource_metadata.test.ts @@ -7,9 +7,10 @@ import { addRequiredKbResourceMetadata } from './add_required_kb_resource_metadata'; import { mockExampleQueryDocsFromDirectoryLoader } from '../../../__mocks__/docs_from_directory_loader'; +import { SECURITY_LABS_RESOURCE } from '../../../routes/knowledge_base/constants'; describe('addRequiredKbResourceMetadata', () => { - const kbResource = 'esql'; + const kbResource = SECURITY_LABS_RESOURCE; test('it includes the original metadata properties', () => { const EXPECTED_ADDITIONAL_KEYS_COUNT = 2; // two keys, `kbResource` and `required` diff --git a/x-pack/plugins/elastic_assistant/server/lib/langchain/content_loaders/esql_loader.test.ts b/x-pack/plugins/elastic_assistant/server/lib/langchain/content_loaders/esql_loader.test.ts deleted file mode 100644 index 9c1c5976fd550..0000000000000 --- a/x-pack/plugins/elastic_assistant/server/lib/langchain/content_loaders/esql_loader.test.ts +++ /dev/null @@ -1,125 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license 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 { addRequiredKbResourceMetadata } from './add_required_kb_resource_metadata'; -import { loadESQL } from './esql_loader'; -import { - mockEsqlDocsFromDirectoryLoader, - mockEsqlLanguageDocsFromDirectoryLoader, - mockExampleQueryDocsFromDirectoryLoader, -} from '../../../__mocks__/docs_from_directory_loader'; -import { ESQL_RESOURCE } from '../../../routes/knowledge_base/constants'; -import { AIAssistantKnowledgeBaseDataClient } from '../../../ai_assistant_data_clients/knowledge_base'; - -let mockLoad = jest.fn(); - -jest.mock('langchain/document_loaders/fs/directory', () => ({ - DirectoryLoader: jest.fn().mockImplementation(() => ({ - load: mockLoad, - })), -})); - -jest.mock('langchain/document_loaders/fs/text', () => ({ - TextLoader: jest.fn().mockImplementation(() => ({})), -})); - -const kbDataClient = { - addKnowledgeBaseDocuments: jest.fn().mockResolvedValue(['1', '2', '3', '4', '5']), -} as unknown as AIAssistantKnowledgeBaseDataClient; - -const logger = { - info: jest.fn(), - error: jest.fn(), -} as unknown as Logger; - -describe('loadESQL', () => { - beforeEach(() => { - jest.clearAllMocks(); - - mockLoad = jest - .fn() - .mockReturnValueOnce(mockEsqlDocsFromDirectoryLoader) - .mockReturnValueOnce(mockEsqlLanguageDocsFromDirectoryLoader) - .mockReturnValueOnce(mockExampleQueryDocsFromDirectoryLoader); - }); - - describe('loadESQL', () => { - beforeEach(async () => { - await loadESQL(kbDataClient, logger); - }); - - it('loads ES|QL docs, language files, and example queries into the Knowledge Base', async () => { - expect(kbDataClient.addKnowledgeBaseDocuments).toHaveBeenCalledWith({ - documents: [ - ...addRequiredKbResourceMetadata({ - docs: mockEsqlDocsFromDirectoryLoader, - kbResource: ESQL_RESOURCE, - required: false, - }), - ...addRequiredKbResourceMetadata({ - docs: mockEsqlLanguageDocsFromDirectoryLoader, - kbResource: ESQL_RESOURCE, - required: false, - }), - ...addRequiredKbResourceMetadata({ - docs: mockExampleQueryDocsFromDirectoryLoader, - kbResource: ESQL_RESOURCE, - required: true, - }), - ], - global: true, - }); - }); - - it('logs the expected (distinct) counts for each category of documents', async () => { - expect((logger.info as jest.Mock).mock.calls[0][0]).toEqual( - 'Loading 1 ES|QL docs, 2 language docs, and 3 example queries into the Knowledge Base' - ); - }); - - it('logs the expected total of all documents loaded', async () => { - expect((logger.info as jest.Mock).mock.calls[1][0]).toEqual( - 'Loaded 5 ES|QL docs, language docs, and example queries into the Knowledge Base' - ); - }); - - it('does NOT log an error in the happy path', async () => { - expect(logger.error).not.toHaveBeenCalled(); - }); - }); - - it('returns true if documents were loaded', async () => { - (kbDataClient.addKnowledgeBaseDocuments as jest.Mock).mockResolvedValueOnce([ - 'this is a response', - ]); - - const result = await loadESQL(kbDataClient, logger); - - expect(result).toBe(true); - }); - - it('returns false if documents were NOT loaded', async () => { - (kbDataClient.addKnowledgeBaseDocuments as jest.Mock).mockResolvedValueOnce([]); - - const result = await loadESQL(kbDataClient, logger); - - expect(result).toBe(false); - }); - - it('logs the expected error if loading fails', async () => { - const error = new Error('Failed to load documents'); - (kbDataClient.addKnowledgeBaseDocuments as jest.Mock).mockRejectedValueOnce(error); - - await loadESQL(kbDataClient, logger); - - expect(logger.error).toHaveBeenCalledWith( - 'Failed to load ES|QL docs, language docs, and example queries into the Knowledge Base\nError: Failed to load documents' - ); - }); -}); diff --git a/x-pack/plugins/elastic_assistant/server/lib/langchain/content_loaders/esql_loader.ts b/x-pack/plugins/elastic_assistant/server/lib/langchain/content_loaders/esql_loader.ts deleted file mode 100644 index 4668671674bc3..0000000000000 --- a/x-pack/plugins/elastic_assistant/server/lib/langchain/content_loaders/esql_loader.ts +++ /dev/null @@ -1,97 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license 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 { DirectoryLoader } from 'langchain/document_loaders/fs/directory'; -import { TextLoader } from 'langchain/document_loaders/fs/text'; -import { resolve } from 'path'; -import { Document } from 'langchain/document'; - -import { Metadata } from '@kbn/elastic-assistant-common'; -import { addRequiredKbResourceMetadata } from './add_required_kb_resource_metadata'; -import { ESQL_RESOURCE } from '../../../routes/knowledge_base/constants'; -import { AIAssistantKnowledgeBaseDataClient } from '../../../ai_assistant_data_clients/knowledge_base'; - -/** - * Loads the ESQL docs and language files into the Knowledge Base. - */ -export const loadESQL = async ( - kbDataClient: AIAssistantKnowledgeBaseDataClient, - logger: Logger -): Promise => { - try { - const docsLoader = new DirectoryLoader( - resolve(__dirname, '../../../knowledge_base/esql/documentation'), - { - '.asciidoc': (path) => new TextLoader(path), - }, - true - ); - - const languageLoader = new DirectoryLoader( - resolve(__dirname, '../../../knowledge_base/esql/language_definition'), - { - '.g4': (path) => new TextLoader(path), - '.tokens': (path) => new TextLoader(path), - }, - true - ); - - const exampleQueriesLoader = new DirectoryLoader( - resolve(__dirname, '../../../knowledge_base/esql/example_queries'), - { - '.asciidoc': (path) => new TextLoader(path), - }, - true - ); - - const docs = (await docsLoader.load()) as Array>; - const languageDocs = (await languageLoader.load()) as Array>; - const rawExampleQueries = await exampleQueriesLoader.load(); - - // Add additional metadata to the example queries that indicates they are required KB documents: - const requiredExampleQueries = addRequiredKbResourceMetadata({ - docs: rawExampleQueries, - kbResource: ESQL_RESOURCE, - }) as Array>; - - // And make sure remaining docs have `kbResource:esql` - const docsWithMetadata = addRequiredKbResourceMetadata({ - docs, - kbResource: ESQL_RESOURCE, - required: false, - }) as Array>; - - const languageDocsWithMetadata = addRequiredKbResourceMetadata({ - docs: languageDocs, - kbResource: ESQL_RESOURCE, - required: false, - }) as Array>; - - logger.info( - `Loading ${docsWithMetadata.length} ES|QL docs, ${languageDocsWithMetadata.length} language docs, and ${requiredExampleQueries.length} example queries into the Knowledge Base` - ); - - const response = await kbDataClient.addKnowledgeBaseDocuments({ - documents: [...docsWithMetadata, ...languageDocsWithMetadata, ...requiredExampleQueries], - global: true, - }); - - logger.info( - `Loaded ${ - response?.length ?? 0 - } ES|QL docs, language docs, and example queries into the Knowledge Base` - ); - - return response.length > 0; - } catch (e) { - logger.error( - `Failed to load ES|QL docs, language docs, and example queries into the Knowledge Base\n${e}` - ); - return false; - } -}; diff --git a/x-pack/plugins/elastic_assistant/server/lib/langchain/elasticsearch_store/elasticsearch_store.test.ts b/x-pack/plugins/elastic_assistant/server/lib/langchain/elasticsearch_store/elasticsearch_store.test.ts index e45ad55999af6..4d32d7bc02da9 100644 --- a/x-pack/plugins/elastic_assistant/server/lib/langchain/elasticsearch_store/elasticsearch_store.test.ts +++ b/x-pack/plugins/elastic_assistant/server/lib/langchain/elasticsearch_store/elasticsearch_store.test.ts @@ -308,18 +308,7 @@ describe('ElasticsearchStore', () => { { query: { bool: { - must_not: [ - { - term: { - 'metadata.kbResource': 'esql', - }, - }, - { - term: { - 'metadata.required': true, - }, - }, - ], + must_not: [{ term: { 'metadata.required': true } }], must: [ { text_expansion: { @@ -340,18 +329,7 @@ describe('ElasticsearchStore', () => { { query: { bool: { - must: [ - { - term: { - 'metadata.kbResource': 'esql', - }, - }, - { - term: { - 'metadata.required': true, - }, - }, - ], + must: [{ term: { 'metadata.required': true } }], }, }, size: TERMS_QUERY_SIZE, @@ -374,18 +352,7 @@ describe('ElasticsearchStore', () => { { query: { bool: { - must_not: [ - { - term: { - 'metadata.kbResource': 'esql', - }, - }, - { - term: { - 'metadata.required': true, - }, - }, - ], + must_not: [{ term: { 'metadata.required': true } }], must: [ { text_expansion: { @@ -406,18 +373,7 @@ describe('ElasticsearchStore', () => { { query: { bool: { - must: [ - { - term: { - 'metadata.kbResource': 'esql', - }, - }, - { - term: { - 'metadata.required': true, - }, - }, - ], + must: [{ term: { 'metadata.required': true } }], }, }, size: TERMS_QUERY_SIZE, @@ -433,7 +389,6 @@ describe('ElasticsearchStore', () => { expect(reportEvent).toHaveBeenCalledWith(KNOWLEDGE_BASE_EXECUTION_SUCCESS_EVENT.eventType, { model: '.elser_model_2', - resourceAccessed: 'esql', responseTime: 142, resultCount: 2, }); @@ -446,7 +401,6 @@ describe('ElasticsearchStore', () => { expect(reportEvent).toHaveBeenCalledWith(KNOWLEDGE_BASE_EXECUTION_ERROR_EVENT.eventType, { model: '.elser_model_2', - resourceAccessed: 'esql', errorMessage: 'Oh no!', }); }); diff --git a/x-pack/plugins/elastic_assistant/server/lib/langchain/elasticsearch_store/elasticsearch_store.ts b/x-pack/plugins/elastic_assistant/server/lib/langchain/elasticsearch_store/elasticsearch_store.ts index 48ea50d9d4fe8..78c1b104685ad 100644 --- a/x-pack/plugins/elastic_assistant/server/lib/langchain/elasticsearch_store/elasticsearch_store.ts +++ b/x-pack/plugins/elastic_assistant/server/lib/langchain/elasticsearch_store/elasticsearch_store.ts @@ -26,7 +26,6 @@ import { getTermsSearchQuery } from './helpers/get_terms_search_query'; import { getVectorSearchQuery } from './helpers/get_vector_search_query'; import type { MsearchResponse } from './helpers/types'; import { - ESQL_RESOURCE, KNOWLEDGE_BASE_INDEX_PATTERN, KNOWLEDGE_BASE_INGEST_PIPELINE, } from '../../../routes/knowledge_base/constants'; @@ -72,7 +71,7 @@ export class ElasticsearchStore extends VectorStore { private readonly logger: Logger; private readonly telemetry: AnalyticsServiceSetup; private readonly model: string; - private kbResource: string; + private kbResource?: string; _vectorstoreType(): string { return 'elasticsearch'; @@ -93,7 +92,7 @@ export class ElasticsearchStore extends VectorStore { this.logger = logger; this.telemetry = telemetry; this.model = model ?? '.elser_model_2'; - this.kbResource = kbResource ?? ESQL_RESOURCE; + this.kbResource = kbResource; this.kbDataClient = kbDataClient; } @@ -269,7 +268,7 @@ export class ElasticsearchStore extends VectorStore { this.telemetry.reportEvent(KNOWLEDGE_BASE_EXECUTION_SUCCESS_EVENT.eventType, { model: this.model, - resourceAccessed: this.kbResource, + ...(this.kbResource != null ? { resourceAccessed: this.kbResource } : {}), resultCount: results.length, responseTime: result.took ?? 0, }); @@ -288,7 +287,7 @@ export class ElasticsearchStore extends VectorStore { const error = transformError(e); this.telemetry.reportEvent(KNOWLEDGE_BASE_EXECUTION_ERROR_EVENT.eventType, { model: this.model, - resourceAccessed: this.kbResource, + ...(this.kbResource != null ? { resourceAccessed: this.kbResource } : {}), errorMessage: error.message, }); this.logger.error(e); diff --git a/x-pack/plugins/elastic_assistant/server/lib/langchain/elasticsearch_store/helpers/get_required_kb_docs_terms_query_dsl.ts b/x-pack/plugins/elastic_assistant/server/lib/langchain/elasticsearch_store/helpers/get_required_kb_docs_terms_query_dsl.ts index ba5af8c3bfef7..df3e8f42ad63b 100644 --- a/x-pack/plugins/elastic_assistant/server/lib/langchain/elasticsearch_store/helpers/get_required_kb_docs_terms_query_dsl.ts +++ b/x-pack/plugins/elastic_assistant/server/lib/langchain/elasticsearch_store/helpers/get_required_kb_docs_terms_query_dsl.ts @@ -20,13 +20,17 @@ import type { Field, FieldValue, QueryDslTermQuery } from '@elastic/elasticsearc * @returns An array of `term`s that may be used in a `bool` Elasticsearch DSL query to filter in/out required KB documents */ export const getRequiredKbDocsTermsQueryDsl = ( - kbResource: string + kbResource?: string ): Array>> => [ - { - term: { - 'metadata.kbResource': kbResource, - }, - }, + ...(kbResource != null + ? [ + { + term: { + 'metadata.kbResource': kbResource, + }, + }, + ] + : []), { term: { 'metadata.required': true, diff --git a/x-pack/plugins/elastic_assistant/server/lib/langchain/graphs/default_assistant_graph/graph.ts b/x-pack/plugins/elastic_assistant/server/lib/langchain/graphs/default_assistant_graph/graph.ts index dba756b9f3c9e..4688caa176b56 100644 --- a/x-pack/plugins/elastic_assistant/server/lib/langchain/graphs/default_assistant_graph/graph.ts +++ b/x-pack/plugins/elastic_assistant/server/lib/langchain/graphs/default_assistant_graph/graph.ts @@ -137,7 +137,7 @@ export const getDefaultAssistantGraph = ({ }) ) .addNode(NodeType.AGENT, (state: AgentState) => - runAgent({ ...nodeParams, state, agentRunnable }) + runAgent({ ...nodeParams, state, agentRunnable, kbDataClient: dataClients?.kbDataClient }) ) .addNode(NodeType.TOOLS, (state: AgentState) => executeTools({ ...nodeParams, state, tools })) .addNode(NodeType.RESPOND, (state: AgentState) => diff --git a/x-pack/plugins/elastic_assistant/server/lib/langchain/graphs/default_assistant_graph/index.ts b/x-pack/plugins/elastic_assistant/server/lib/langchain/graphs/default_assistant_graph/index.ts index ada5b8a421441..4f043c681f8df 100644 --- a/x-pack/plugins/elastic_assistant/server/lib/langchain/graphs/default_assistant_graph/index.ts +++ b/x-pack/plugins/elastic_assistant/server/lib/langchain/graphs/default_assistant_graph/index.ts @@ -103,7 +103,6 @@ export const callAssistantGraph: AgentExecutor = async ({ isEnabledKnowledgeBase, kbDataClient: dataClients?.kbDataClient, logger, - modelExists: isEnabledKnowledgeBase, onNewReplacements, replacements, request, diff --git a/x-pack/plugins/elastic_assistant/server/lib/langchain/graphs/default_assistant_graph/nodes/run_agent.ts b/x-pack/plugins/elastic_assistant/server/lib/langchain/graphs/default_assistant_graph/nodes/run_agent.ts index 2d076f6bd1472..053254a1d99b3 100644 --- a/x-pack/plugins/elastic_assistant/server/lib/langchain/graphs/default_assistant_graph/nodes/run_agent.ts +++ b/x-pack/plugins/elastic_assistant/server/lib/langchain/graphs/default_assistant_graph/nodes/run_agent.ts @@ -10,15 +10,20 @@ import { AgentRunnableSequence } from 'langchain/dist/agents/agent'; import { formatLatestUserMessage } from '../prompts'; import { AgentState, NodeParamsBase } from '../types'; import { NodeType } from '../constants'; +import { AIAssistantKnowledgeBaseDataClient } from '../../../../../ai_assistant_data_clients/knowledge_base'; export interface RunAgentParams extends NodeParamsBase { state: AgentState; config?: RunnableConfig; agentRunnable: AgentRunnableSequence; + kbDataClient?: AIAssistantKnowledgeBaseDataClient; } export const AGENT_NODE_TAG = 'agent_run'; +const KNOWLEDGE_HISTORY_PREFIX = 'Knowledge History:'; +const NO_KNOWLEDGE_HISTORY = '[No existing knowledge history]'; + /** * Node to run the agent * @@ -26,18 +31,27 @@ export const AGENT_NODE_TAG = 'agent_run'; * @param state - The current state of the graph * @param config - Any configuration that may've been supplied * @param agentRunnable - The agent to run + * @param kbDataClient - Data client for accessing the Knowledge Base on behalf of the current user */ export async function runAgent({ logger, state, agentRunnable, config, + kbDataClient, }: RunAgentParams): Promise> { logger.debug(() => `${NodeType.AGENT}: Node state:\n${JSON.stringify(state, null, 2)}`); + const knowledgeHistory = await kbDataClient?.getRequiredKnowledgeBaseDocumentEntries(); + const agentOutcome = await agentRunnable.withConfig({ tags: [AGENT_NODE_TAG] }).invoke( { ...state, + knowledge_history: `${KNOWLEDGE_HISTORY_PREFIX}\n${ + knowledgeHistory?.length + ? JSON.stringify(knowledgeHistory.map((e) => e.text)) + : NO_KNOWLEDGE_HISTORY + }`, // prepend any user prompt (gemini) input: formatLatestUserMessage(state.input, state.llmType), chat_history: state.messages, // TODO: Message de-dupe with ...state spread diff --git a/x-pack/plugins/elastic_assistant/server/lib/langchain/graphs/default_assistant_graph/nodes/translations.ts b/x-pack/plugins/elastic_assistant/server/lib/langchain/graphs/default_assistant_graph/nodes/translations.ts index e55e1081e6474..e5a1c14846e23 100644 --- a/x-pack/plugins/elastic_assistant/server/lib/langchain/graphs/default_assistant_graph/nodes/translations.ts +++ b/x-pack/plugins/elastic_assistant/server/lib/langchain/graphs/default_assistant_graph/nodes/translations.ts @@ -8,8 +8,10 @@ const YOU_ARE_A_HELPFUL_EXPERT_ASSISTANT = 'You are a security analyst and expert in resolving security incidents. Your role is to assist by answering questions about Elastic Security.'; const IF_YOU_DONT_KNOW_THE_ANSWER = 'Do not answer questions unrelated to Elastic Security.'; +export const KNOWLEDGE_HISTORY = + 'If available, use the Knowledge History provided to try and answer the question. If not provided, you can try and query for additional knowledge via the KnowledgeBaseRetrievalTool.'; -export const DEFAULT_SYSTEM_PROMPT = `${YOU_ARE_A_HELPFUL_EXPERT_ASSISTANT} ${IF_YOU_DONT_KNOW_THE_ANSWER}`; +export const DEFAULT_SYSTEM_PROMPT = `${YOU_ARE_A_HELPFUL_EXPERT_ASSISTANT} ${IF_YOU_DONT_KNOW_THE_ANSWER} ${KNOWLEDGE_HISTORY}`; // system prompt from @afirstenberg const BASE_GEMINI_PROMPT = 'You are an assistant that is an expert at using tools and Elastic Security, doing your best to use these tools to answer questions or follow instructions. It is very important to use tools to answer the question or follow the instructions rather than coming up with your own answer. Tool calls are good. Sometimes you may need to make several tool calls to accomplish the task or get an answer to the question that was asked. Use as many tool calls as necessary.'; @@ -19,7 +21,7 @@ export const GEMINI_SYSTEM_PROMPT = `${BASE_GEMINI_PROMPT} ${KB_CATCH}`; export const BEDROCK_SYSTEM_PROMPT = `Use tools as often as possible, as they have access to the latest data and syntax. Always return value from ESQLKnowledgeBaseTool as is. Never return tags in the response, but make sure to include tags content in the response. Do not reflect on the quality of the returned search results in your response.`; export const GEMINI_USER_PROMPT = `Now, always using the tools at your disposal, step by step, come up with a response to this request:\n\n`; -export const STRUCTURED_SYSTEM_PROMPT = `Respond to the human as helpfully and accurately as possible. You have access to the following tools: +export const STRUCTURED_SYSTEM_PROMPT = `Respond to the human as helpfully and accurately as possible. ${KNOWLEDGE_HISTORY} You have access to the following tools: {tools} diff --git a/x-pack/plugins/elastic_assistant/server/lib/langchain/graphs/default_assistant_graph/prompts.ts b/x-pack/plugins/elastic_assistant/server/lib/langchain/graphs/default_assistant_graph/prompts.ts index 883047ed7b9df..05cc8b50852f5 100644 --- a/x-pack/plugins/elastic_assistant/server/lib/langchain/graphs/default_assistant_graph/prompts.ts +++ b/x-pack/plugins/elastic_assistant/server/lib/langchain/graphs/default_assistant_graph/prompts.ts @@ -17,6 +17,7 @@ import { export const formatPrompt = (prompt: string, additionalPrompt?: string) => ChatPromptTemplate.fromMessages([ ['system', additionalPrompt ? `${prompt}\n\n${additionalPrompt}` : prompt], + ['placeholder', '{knowledge_history}'], ['placeholder', '{chat_history}'], ['human', '{input}'], ['placeholder', '{agent_scratchpad}'], @@ -39,6 +40,7 @@ export const geminiToolCallingAgentPrompt = formatPrompt(systemPrompts.gemini); export const formatPromptStructured = (prompt: string, additionalPrompt?: string) => ChatPromptTemplate.fromMessages([ ['system', additionalPrompt ? `${prompt}\n\n${additionalPrompt}` : prompt], + ['placeholder', '{knowledge_history}'], ['placeholder', '{chat_history}'], [ 'human', diff --git a/x-pack/plugins/elastic_assistant/server/lib/telemetry/event_based_telemetry.ts b/x-pack/plugins/elastic_assistant/server/lib/telemetry/event_based_telemetry.ts index 4ba95896d7058..5ff5ff894dffe 100644 --- a/x-pack/plugins/elastic_assistant/server/lib/telemetry/event_based_telemetry.ts +++ b/x-pack/plugins/elastic_assistant/server/lib/telemetry/event_based_telemetry.ts @@ -9,7 +9,7 @@ import type { EventTypeOpts } from '@kbn/core/server'; export const KNOWLEDGE_BASE_EXECUTION_SUCCESS_EVENT: EventTypeOpts<{ model: string; - resourceAccessed: string; + resourceAccessed?: string; resultCount: number; responseTime: number; }> = { @@ -25,6 +25,7 @@ export const KNOWLEDGE_BASE_EXECUTION_SUCCESS_EVENT: EventTypeOpts<{ type: 'keyword', _meta: { description: 'Which knowledge base resource was accessed', + optional: true, }, }, resultCount: { @@ -44,7 +45,7 @@ export const KNOWLEDGE_BASE_EXECUTION_SUCCESS_EVENT: EventTypeOpts<{ export const KNOWLEDGE_BASE_EXECUTION_ERROR_EVENT: EventTypeOpts<{ model: string; - resourceAccessed: string; + resourceAccessed?: string; errorMessage: string; }> = { eventType: 'knowledge_base_execution_error', @@ -59,6 +60,7 @@ export const KNOWLEDGE_BASE_EXECUTION_ERROR_EVENT: EventTypeOpts<{ type: 'keyword', _meta: { description: 'Which knowledge base resource was accessed', + optional: true, }, }, errorMessage: { diff --git a/x-pack/plugins/elastic_assistant/server/routes/attack_discovery/helpers.test.ts b/x-pack/plugins/elastic_assistant/server/routes/attack_discovery/helpers.test.ts index 15877e6727715..d5eaf7d159618 100644 --- a/x-pack/plugins/elastic_assistant/server/routes/attack_discovery/helpers.test.ts +++ b/x-pack/plugins/elastic_assistant/server/routes/attack_discovery/helpers.test.ts @@ -196,7 +196,6 @@ describe('helpers', () => { langChainTimeout, llm, logger: mockLogger, - modelExists: false, onNewReplacements, replacements: latestReplacements, request: mockRequest, @@ -231,7 +230,6 @@ describe('helpers', () => { langChainTimeout, llm, logger: mockLogger, - modelExists: false, onNewReplacements, replacements: latestReplacements, request: mockRequest, diff --git a/x-pack/plugins/elastic_assistant/server/routes/attack_discovery/helpers.ts b/x-pack/plugins/elastic_assistant/server/routes/attack_discovery/helpers.ts index 2a1450a9f7b9b..f016d6ac29118 100644 --- a/x-pack/plugins/elastic_assistant/server/routes/attack_discovery/helpers.ts +++ b/x-pack/plugins/elastic_assistant/server/routes/attack_discovery/helpers.ts @@ -157,7 +157,6 @@ const formatAssistantToolParams = ({ langChainTimeout, llm, logger, - modelExists: false, // not required for attack discovery onNewReplacements, replacements: latestReplacements, request, diff --git a/x-pack/plugins/elastic_assistant/server/routes/evaluate/post_evaluate.ts b/x-pack/plugins/elastic_assistant/server/routes/evaluate/post_evaluate.ts index de154a1ddd96d..29a7527964677 100644 --- a/x-pack/plugins/elastic_assistant/server/routes/evaluate/post_evaluate.ts +++ b/x-pack/plugins/elastic_assistant/server/routes/evaluate/post_evaluate.ts @@ -236,7 +236,6 @@ export const postEvaluateRoute = ( llm, isOssModel, logger, - modelExists: isEnabledKnowledgeBase, request: skeletonRequest, alertsIndexPattern, // onNewReplacements, diff --git a/x-pack/plugins/elastic_assistant/server/routes/knowledge_base/constants.ts b/x-pack/plugins/elastic_assistant/server/routes/knowledge_base/constants.ts index e50faf8a434e2..89970611df0e9 100644 --- a/x-pack/plugins/elastic_assistant/server/routes/knowledge_base/constants.ts +++ b/x-pack/plugins/elastic_assistant/server/routes/knowledge_base/constants.ts @@ -5,13 +5,10 @@ * 2.0. */ -export const MODEL_EVALUATION_RESULTS_INDEX_PATTERN = - '.kibana-elastic-ai-assistant-evaluation-results'; export const KNOWLEDGE_BASE_INDEX_PATTERN = '.kibana-elastic-ai-assistant-kb'; export const KNOWLEDGE_BASE_INGEST_PIPELINE = '.kibana-elastic-ai-assistant-kb-ingest-pipeline'; // Query for determining if ESQL docs have been loaded, searches for a specific doc. Intended for the ElasticsearchStore.similaritySearch() // Note: We may want to add a tag of the resource name to the document metadata, so we can CRUD by specific resource export const ESQL_DOCS_LOADED_QUERY = 'You can chain processing commands, separated by a pipe character: `|`.'; -export const ESQL_RESOURCE = 'esql'; export const SECURITY_LABS_RESOURCE = 'security_labs'; diff --git a/x-pack/plugins/elastic_assistant/server/routes/knowledge_base/entries/bulk_actions_route.ts b/x-pack/plugins/elastic_assistant/server/routes/knowledge_base/entries/bulk_actions_route.ts index 96045b17e6171..ce3f0c8c92693 100644 --- a/x-pack/plugins/elastic_assistant/server/routes/knowledge_base/entries/bulk_actions_route.ts +++ b/x-pack/plugins/elastic_assistant/server/routes/knowledge_base/entries/bulk_actions_route.ts @@ -22,11 +22,18 @@ import { buildRouteValidationWithZod } from '@kbn/elastic-assistant-common/impl/ import { performChecks } from '../../helpers'; import { KNOWLEDGE_BASE_ENTRIES_TABLE_MAX_PAGE_SIZE } from '../../../../common/constants'; -import { EsKnowledgeBaseEntrySchema } from '../../../ai_assistant_data_clients/knowledge_base/types'; +import { + EsKnowledgeBaseEntrySchema, + UpdateKnowledgeBaseEntrySchema, +} from '../../../ai_assistant_data_clients/knowledge_base/types'; import { ElasticAssistantPluginRouter } from '../../../types'; import { buildResponse } from '../../utils'; import { transformESSearchToKnowledgeBaseEntry } from '../../../ai_assistant_data_clients/knowledge_base/transforms'; -import { transformToCreateSchema } from '../../../ai_assistant_data_clients/knowledge_base/create_knowledge_base_entry'; +import { + getUpdateScript, + transformToCreateSchema, + transformToUpdateSchema, +} from '../../../ai_assistant_data_clients/knowledge_base/create_knowledge_base_entry'; export interface BulkOperationError { message: string; @@ -210,7 +217,17 @@ export const bulkActionKnowledgeBaseEntriesRoute = (router: ElasticAssistantPlug }) ), documentsToDelete: body.delete?.ids, - documentsToUpdate: [], // TODO: Support bulk update + documentsToUpdate: body.update?.map((entry) => + // TODO: KB-RBAC check, required when users != null as entry will either be created globally if empty + transformToUpdateSchema({ + user: authenticatedUser, + updatedAt: changedAt, + entry, + global: entry.users != null && entry.users.length === 0, + }) + ), + getUpdateScript: (entry: UpdateKnowledgeBaseEntrySchema) => + getUpdateScript({ entry, isPatch: true }), authenticatedUser, }); const created = diff --git a/x-pack/plugins/elastic_assistant/server/routes/knowledge_base/entries/create_route.ts b/x-pack/plugins/elastic_assistant/server/routes/knowledge_base/entries/create_route.ts index 3dbb5a9cf930e..51e3d48505ec2 100644 --- a/x-pack/plugins/elastic_assistant/server/routes/knowledge_base/entries/create_route.ts +++ b/x-pack/plugins/elastic_assistant/server/routes/knowledge_base/entries/create_route.ts @@ -66,7 +66,7 @@ export const createKnowledgeBaseEntryRoute = (router: ElasticAssistantPluginRout logger.debug(() => `Creating KB Entry:\n${JSON.stringify(request.body)}`); const createResponse = await kbDataClient?.createKnowledgeBaseEntry({ knowledgeBaseEntry: request.body, - // TODO: KB-RBAC check, required when users != null as entry will either be created globally if empty, or for specific users (only admin API feature) + // TODO: KB-RBAC check, required when users != null as entry will either be created globally if empty global: request.body.users != null && request.body.users.length === 0, }); diff --git a/x-pack/plugins/elastic_assistant/server/routes/knowledge_base/entries/find_route.ts b/x-pack/plugins/elastic_assistant/server/routes/knowledge_base/entries/find_route.ts index 78b7ac720dd1a..356d5d9150a67 100644 --- a/x-pack/plugins/elastic_assistant/server/routes/knowledge_base/entries/find_route.ts +++ b/x-pack/plugins/elastic_assistant/server/routes/knowledge_base/entries/find_route.ts @@ -26,7 +26,7 @@ import { performChecks } from '../../helpers'; import { transformESSearchToKnowledgeBaseEntry } from '../../../ai_assistant_data_clients/knowledge_base/transforms'; import { EsKnowledgeBaseEntrySchema } from '../../../ai_assistant_data_clients/knowledge_base/types'; import { getKBUserFilter } from './utils'; -import { ESQL_RESOURCE, SECURITY_LABS_RESOURCE } from '../constants'; +import { SECURITY_LABS_RESOURCE } from '../constants'; export const findKnowledgeBaseEntriesRoute = (router: ElasticAssistantPluginRouter) => { router.versioned @@ -74,7 +74,7 @@ export const findKnowledgeBaseEntriesRoute = (router: ElasticAssistantPluginRout }); const currentUser = ctx.elasticAssistant.getCurrentUser(); const userFilter = getKBUserFilter(currentUser); - const systemFilter = ` AND kb_resource:"user"`; + const systemFilter = ` AND (kb_resource:"user" OR type:"index")`; const additionalFilter = query.filter ? ` AND ${query.filter}` : ''; const result = await kbDataClient?.findDocuments({ @@ -108,12 +108,6 @@ export const findKnowledgeBaseEntriesRoute = (router: ElasticAssistantPluginRout }); const systemEntries = [ - { - bucketId: 'esqlDocsId', - kbResource: ESQL_RESOURCE, - name: 'ES|QL documents', - required: true, - }, { bucketId: 'securityLabsId', kbResource: SECURITY_LABS_RESOURCE, @@ -166,7 +160,7 @@ export const findKnowledgeBaseEntriesRoute = (router: ElasticAssistantPluginRout body: { perPage: result.perPage, page: result.page, - total: result.total, + total: result.total + systemEntries.length, data: [...transformESSearchToKnowledgeBaseEntry(result.data), ...systemEntries], }, }); diff --git a/x-pack/plugins/elastic_assistant/server/routes/knowledge_base/get_knowledge_base_status.test.ts b/x-pack/plugins/elastic_assistant/server/routes/knowledge_base/get_knowledge_base_status.test.ts index 7f1d1d0149f56..6244599a2af27 100644 --- a/x-pack/plugins/elastic_assistant/server/routes/knowledge_base/get_knowledge_base_status.test.ts +++ b/x-pack/plugins/elastic_assistant/server/routes/knowledge_base/get_knowledge_base_status.test.ts @@ -36,6 +36,8 @@ describe('Get Knowledge Base Status Route', () => { isModelInstalled: jest.fn().mockResolvedValue(true), isSetupAvailable: jest.fn().mockResolvedValue(true), isModelDeployed: jest.fn().mockResolvedValue(true), + isSetupInProgress: false, + isSecurityLabsDocsLoaded: jest.fn().mockResolvedValue(true), }); getKnowledgeBaseStatusRoute(server.router); @@ -44,10 +46,19 @@ describe('Get Knowledge Base Status Route', () => { describe('Status codes', () => { test('returns 200 if all statuses are false', async () => { const response = await server.inject( - getGetKnowledgeBaseStatusRequest('esql'), + getGetKnowledgeBaseStatusRequest(), requestContextMock.convertContext(context) ); + expect(response.status).toEqual(200); + expect(response.body).toEqual({ + elser_exists: true, + index_exists: true, + is_setup_in_progress: false, + is_setup_available: true, + pipeline_exists: true, + security_labs_exists: true, + }); }); }); }); diff --git a/x-pack/plugins/elastic_assistant/server/routes/knowledge_base/get_knowledge_base_status.ts b/x-pack/plugins/elastic_assistant/server/routes/knowledge_base/get_knowledge_base_status.ts index 3e4fcbb7f404b..833e674b68ffd 100644 --- a/x-pack/plugins/elastic_assistant/server/routes/knowledge_base/get_knowledge_base_status.ts +++ b/x-pack/plugins/elastic_assistant/server/routes/knowledge_base/get_knowledge_base_status.ts @@ -15,10 +15,8 @@ import { } from '@kbn/elastic-assistant-common'; import { buildRouteValidationWithZod } from '@kbn/elastic-assistant-common/impl/schemas/common'; import { KibanaRequest } from '@kbn/core/server'; -import { getKbResource } from './get_kb_resource'; import { buildResponse } from '../../lib/build_response'; import { ElasticAssistantPluginRouter } from '../../types'; -import { ESQL_RESOURCE } from './constants'; import { isV2KnowledgeBaseEnabled } from '../helpers'; /** @@ -51,9 +49,6 @@ export const getKnowledgeBaseStatusRoute = (router: ElasticAssistantPluginRouter const logger = ctx.elasticAssistant.logger; try { - // Use asInternalUser - const kbResource = getKbResource(request); - // FF Check for V2 KB const v2KnowledgeBaseEnabled = isV2KnowledgeBaseEnabled({ context: ctx, request }); @@ -78,13 +73,11 @@ export const getKnowledgeBaseStatusRoute = (router: ElasticAssistantPluginRouter pipeline_exists: pipelineExists, }; - if (indexExists && isModelDeployed && kbResource === ESQL_RESOURCE) { - const esqlExists = await kbDataClient.isESQLDocsLoaded(); + if (indexExists && isModelDeployed) { const securityLabsExists = await kbDataClient.isSecurityLabsDocsLoaded(); return response.ok({ body: { ...body, - esql_exists: esqlExists, security_labs_exists: v2KnowledgeBaseEnabled ? securityLabsExists : true, }, }); diff --git a/x-pack/plugins/elastic_assistant/server/routes/knowledge_base/post_knowledge_base.ts b/x-pack/plugins/elastic_assistant/server/routes/knowledge_base/post_knowledge_base.ts index b5abf30b2bf07..e57018cac3706 100644 --- a/x-pack/plugins/elastic_assistant/server/routes/knowledge_base/post_knowledge_base.ts +++ b/x-pack/plugins/elastic_assistant/server/routes/knowledge_base/post_knowledge_base.ts @@ -17,7 +17,6 @@ import { IKibanaResponse } from '@kbn/core/server'; import { buildResponse } from '../../lib/build_response'; import { ElasticAssistantPluginRouter } from '../../types'; import { isV2KnowledgeBaseEnabled } from '../helpers'; -import { ESQL_RESOURCE } from './constants'; // Since we're awaiting on ELSER setup, this could take a bit (especially if ML needs to autoscale) // Consider just returning if attempt was successful, and switch to client polling @@ -55,7 +54,6 @@ export const postKnowledgeBaseRoute = (router: ElasticAssistantPluginRouter) => const assistantContext = ctx.elasticAssistant; const core = ctx.core; const soClient = core.savedObjects.getClient(); - const kbResource = request.params.resource; // FF Check for V2 KB const v2KnowledgeBaseEnabled = isV2KnowledgeBaseEnabled({ context: ctx, request }); @@ -73,10 +71,8 @@ export const postKnowledgeBaseRoute = (router: ElasticAssistantPluginRouter) => return response.custom({ body: { success: false }, statusCode: 500 }); } - const installEsqlDocs = kbResource === ESQL_RESOURCE; await knowledgeBaseDataClient.setupKnowledgeBase({ soClient, - installEsqlDocs, installSecurityLabsDocs: v2KnowledgeBaseEnabled, }); diff --git a/x-pack/plugins/elastic_assistant/server/routes/utils.test.ts b/x-pack/plugins/elastic_assistant/server/routes/utils.test.ts index 3ca1b8edb5036..9a77e645686dd 100644 --- a/x-pack/plugins/elastic_assistant/server/routes/utils.test.ts +++ b/x-pack/plugins/elastic_assistant/server/routes/utils.test.ts @@ -65,5 +65,17 @@ describe('Utils', () => { const isOpenModel = isOpenSourceModel(connector); expect(isOpenModel).toEqual(true); }); + + it('should return `true` when apiProvider of OpenAiProviderType.Other is specified', async () => { + const connector = { + actionTypeId: '.gen-ai', + config: { + apiUrl: OPENAI_CHAT_URL, + apiProvider: OpenAiProviderType.Other, + }, + } as unknown as Connector; + const isOpenModel = isOpenSourceModel(connector); + expect(isOpenModel).toEqual(true); + }); }); }); diff --git a/x-pack/plugins/elastic_assistant/server/routes/utils.ts b/x-pack/plugins/elastic_assistant/server/routes/utils.ts index ea05fc814ec69..0fb51c7364809 100644 --- a/x-pack/plugins/elastic_assistant/server/routes/utils.ts +++ b/x-pack/plugins/elastic_assistant/server/routes/utils.ts @@ -203,19 +203,25 @@ export const isOpenSourceModel = (connector?: Connector): boolean => { } const llmType = getLlmType(connector.actionTypeId); - const connectorApiUrl = connector.config?.apiUrl - ? (connector.config.apiUrl as string) - : undefined; + const isOpenAiType = llmType === 'openai'; + + if (!isOpenAiType) { + return false; + } const connectorApiProvider = connector.config?.apiProvider ? (connector.config?.apiProvider as OpenAiProviderType) : undefined; + if (connectorApiProvider === OpenAiProviderType.Other) { + return true; + } - const isOpenAiType = llmType === 'openai'; - const isOpenAI = - isOpenAiType && - (!connectorApiUrl || - connectorApiUrl === OPENAI_CHAT_URL || - connectorApiProvider === OpenAiProviderType.AzureAi); + const connectorApiUrl = connector.config?.apiUrl + ? (connector.config.apiUrl as string) + : undefined; - return isOpenAiType && !isOpenAI; + return ( + !!connectorApiUrl && + connectorApiUrl !== OPENAI_CHAT_URL && + connectorApiProvider !== OpenAiProviderType.AzureAi + ); }; diff --git a/x-pack/plugins/elastic_assistant/server/types.ts b/x-pack/plugins/elastic_assistant/server/types.ts index 3117295810877..45bd5a4149b58 100755 --- a/x-pack/plugins/elastic_assistant/server/types.ts +++ b/x-pack/plugins/elastic_assistant/server/types.ts @@ -244,7 +244,6 @@ export interface AssistantToolParams { llm?: ActionsClientLlm | AssistantToolLlm; isOssModel?: boolean; logger: Logger; - modelExists: boolean; onNewReplacements?: (newReplacements: Replacements) => void; replacements?: Replacements; request: KibanaRequest< diff --git a/x-pack/plugins/encrypted_saved_objects/server/routes/index.mock.ts b/x-pack/plugins/encrypted_saved_objects/server/routes/index.mock.ts index e6263521f690d..4d453f64b6954 100644 --- a/x-pack/plugins/encrypted_saved_objects/server/routes/index.mock.ts +++ b/x-pack/plugins/encrypted_saved_objects/server/routes/index.mock.ts @@ -5,7 +5,7 @@ * 2.0. */ -import { BuildFlavor } from '@kbn/config'; +import type { BuildFlavor } from '@kbn/config'; import { httpServiceMock, loggingSystemMock } from '@kbn/core/server/mocks'; import type { ConfigType } from '../config'; diff --git a/x-pack/plugins/encrypted_saved_objects/server/routes/index.ts b/x-pack/plugins/encrypted_saved_objects/server/routes/index.ts index 14d1933e8b765..aa3499ee46055 100644 --- a/x-pack/plugins/encrypted_saved_objects/server/routes/index.ts +++ b/x-pack/plugins/encrypted_saved_objects/server/routes/index.ts @@ -5,7 +5,7 @@ * 2.0. */ -import { BuildFlavor } from '@kbn/config'; +import type { BuildFlavor } from '@kbn/config'; import type { IRouter, Logger } from '@kbn/core/server'; import type { PublicMethodsOf } from '@kbn/utility-types'; diff --git a/x-pack/plugins/enterprise_search/public/navigation_tree.ts b/x-pack/plugins/enterprise_search/public/navigation_tree.ts index c893c56f372c7..2f4e5a17ab335 100644 --- a/x-pack/plugins/enterprise_search/public/navigation_tree.ts +++ b/x-pack/plugins/enterprise_search/public/navigation_tree.ts @@ -93,7 +93,7 @@ export const getNavigationTreeDefinition = ({ return pathNameSerialized.startsWith(prepend('/app/dev_tools')); }, id: 'dev_tools', - link: 'dev_tools:console', + link: 'dev_tools', title: i18n.translate('xpack.enterpriseSearch.searchNav.devTools', { defaultMessage: 'Dev Tools', }), @@ -218,7 +218,11 @@ export const getNavigationTreeDefinition = ({ { children: [ { - getIsActive: () => false, + getIsActive: ({ pathNameSerialized, prepend }) => { + return pathNameSerialized.startsWith( + prepend('/app/enterprise_search/app_search') + ); + }, link: 'appSearch:engines', title: i18n.translate( 'xpack.enterpriseSearch.searchNav.entsearch.appSearch', @@ -235,7 +239,11 @@ export const getNavigationTreeDefinition = ({ : {}), }, { - getIsActive: () => false, + getIsActive: ({ pathNameSerialized, prepend }) => { + return pathNameSerialized.startsWith( + prepend('/app/enterprise_search/workplace_search') + ); + }, link: 'workplaceSearch', ...(workplaceSearch ? { diff --git a/x-pack/plugins/entity_manager/common/constants_entities.ts b/x-pack/plugins/entity_manager/common/constants_entities.ts index c53847afbb548..c17e6f33918c6 100644 --- a/x-pack/plugins/entity_manager/common/constants_entities.ts +++ b/x-pack/plugins/entity_manager/common/constants_entities.ts @@ -33,8 +33,6 @@ export const ENTITY_LATEST_PREFIX_V1 = `${ENTITY_BASE_PREFIX}-${ENTITY_SCHEMA_VERSION_V1}-${ENTITY_LATEST}` as const; // Transform constants -export const ENTITY_DEFAULT_HISTORY_FREQUENCY = '1m'; -export const ENTITY_DEFAULT_HISTORY_SYNC_DELAY = '60s'; -export const ENTITY_DEFAULT_LATEST_FREQUENCY = '30s'; -export const ENTITY_DEFAULT_LATEST_SYNC_DELAY = '1s'; -export const ENTITY_DEFAULT_METADATA_LIMIT = 1000; +export const ENTITY_DEFAULT_LATEST_FREQUENCY = '1m'; +export const ENTITY_DEFAULT_LATEST_SYNC_DELAY = '60s'; +export const ENTITY_DEFAULT_METADATA_LIMIT = 10; diff --git a/x-pack/plugins/entity_manager/server/lib/entities/built_in/containers_from_ecs_data.ts b/x-pack/plugins/entity_manager/server/lib/entities/built_in/containers_from_ecs_data.ts index b9119143ec37e..e3356c4826ae8 100644 --- a/x-pack/plugins/entity_manager/server/lib/entities/built_in/containers_from_ecs_data.ts +++ b/x-pack/plugins/entity_manager/server/lib/entities/built_in/containers_from_ecs_data.ts @@ -12,7 +12,7 @@ export const builtInContainersFromEcsEntityDefinition: EntityDefinition = entityDefinitionSchema.parse({ id: `${BUILT_IN_ID_PREFIX}containers_from_ecs_data`, managed: true, - version: '1.0.0', + version: '0.1.0', name: 'Containers from ECS data', description: 'This definition extracts container entities from common data streams by looking for the ECS field container.id', @@ -20,9 +20,9 @@ export const builtInContainersFromEcsEntityDefinition: EntityDefinition = indexPatterns: ['filebeat-*', 'logs-*', 'metrics-*', 'metricbeat-*'], identityFields: ['container.id'], displayNameTemplate: '{{container.id}}', - history: { + latest: { timestampField: '@timestamp', - interval: '5m', + lookbackPeriod: '10m', settings: { frequency: '5m', }, @@ -65,94 +65,4 @@ export const builtInContainersFromEcsEntityDefinition: EntityDefinition = 'agent.type', 'agent.ephemeral_id', ], - metrics: [ - { - name: 'log_rate', - equation: 'A', - metrics: [ - { - name: 'A', - aggregation: 'doc_count', - filter: 'log.level: * OR error.log.level: *', - }, - ], - }, - { - name: 'error_log_rate', - equation: 'A', - metrics: [ - { - name: 'A', - aggregation: 'doc_count', - filter: '(log.level: "error" OR "ERROR") OR (error.log.level: "error" OR "ERROR")', - }, - ], - }, - { - name: 'cpu_usage_avg', - equation: 'A', - metrics: [ - { - name: 'A', - aggregation: 'avg', - field: 'docker.cpu.total.pct', - }, - ], - }, - { - name: 'memory_usage_avg', - equation: 'A', - metrics: [ - { - name: 'A', - aggregation: 'avg', - field: 'docker.memory.usage.pct', - }, - ], - }, - { - name: 'network_in_avg', - equation: 'A', - metrics: [ - { - name: 'A', - aggregation: 'avg', - field: 'docker.network.in.bytes', - }, - ], - }, - { - name: 'network_out_avg', - equation: 'A', - metrics: [ - { - name: 'A', - aggregation: 'avg', - field: 'docker.network.out.bytes', - }, - ], - }, - { - name: 'disk_read_avg', - equation: 'A', - metrics: [ - { - name: 'A', - aggregation: 'avg', - field: 'docker.diskio.read.ops', - }, - ], - }, - { - name: 'disk_write_avg', - equation: 'A', - metrics: [ - { - name: 'A', - aggregation: 'avg', - field: 'docker.diskio.write.ops', - }, - ], - }, - ], }); diff --git a/x-pack/plugins/entity_manager/server/lib/entities/built_in/hosts_from_ecs_data.ts b/x-pack/plugins/entity_manager/server/lib/entities/built_in/hosts_from_ecs_data.ts index 5fead32f5c0e8..5d7a30093419e 100644 --- a/x-pack/plugins/entity_manager/server/lib/entities/built_in/hosts_from_ecs_data.ts +++ b/x-pack/plugins/entity_manager/server/lib/entities/built_in/hosts_from_ecs_data.ts @@ -11,7 +11,7 @@ import { BUILT_IN_ID_PREFIX } from './constants'; export const builtInHostsFromEcsEntityDefinition: EntityDefinition = entityDefinitionSchema.parse({ id: `${BUILT_IN_ID_PREFIX}hosts_from_ecs_data`, managed: true, - version: '1.0.0', + version: '0.1.0', name: 'Hosts from ECS data', description: 'This definition extracts host entities from common data streams by looking for the ECS field host.name', @@ -19,9 +19,9 @@ export const builtInHostsFromEcsEntityDefinition: EntityDefinition = entityDefin indexPatterns: ['filebeat-*', 'logs-*', 'metrics-*', 'metricbeat-*'], identityFields: ['host.name'], displayNameTemplate: '{{host.name}}', - history: { + latest: { timestampField: '@timestamp', - interval: '5m', + lookbackPeriod: '10m', settings: { frequency: '5m', }, @@ -65,115 +65,4 @@ export const builtInHostsFromEcsEntityDefinition: EntityDefinition = entityDefin 'agent.type', 'agent.version', ], - metrics: [ - { - name: 'log_rate', - equation: 'A', - metrics: [ - { - name: 'A', - aggregation: 'doc_count', - filter: 'log.level: * OR error.log.level: *', - }, - ], - }, - { - name: 'error_log_rate', - equation: 'A', - metrics: [ - { - name: 'A', - aggregation: 'doc_count', - filter: '(log.level: "error" OR "ERROR") OR (error.log.level: "error" OR "ERROR")', - }, - ], - }, - { - name: 'cpu_usage_avg', - equation: 'A', - metrics: [ - { - name: 'A', - aggregation: 'avg', - field: 'system.cpu.total.norm.pct', - }, - ], - }, - { - name: 'normalized_load_avg', - equation: 'A / B', - metrics: [ - { - name: 'A', - aggregation: 'avg', - field: 'system.load.1', - }, - { - name: 'B', - aggregation: 'max', - field: 'system.load.cores', - }, - ], - }, - { - name: 'memory_usage_avg', - equation: 'A', - metrics: [ - { - name: 'A', - aggregation: 'avg', - field: 'system.memory.actual.used.pct', - }, - ], - }, - { - name: 'memory_free_avg', - equation: 'A - B', - metrics: [ - { - name: 'A', - aggregation: 'max', - field: 'system.memory.total', - }, - { - name: 'B', - aggregation: 'avg', - field: 'system.memory.actual.used.bytes', - }, - ], - }, - { - name: 'disk_usage_max', - equation: 'A', - metrics: [ - { - name: 'A', - aggregation: 'max', - field: 'system.filesystem.used.pct', - }, - ], - }, - { - name: 'rx_avg', - equation: 'A * 8', - metrics: [ - { - name: 'A', - aggregation: 'sum', - field: 'host.network.ingress.bytes', - }, - ], - }, - { - name: 'tx_avg', - equation: 'A * 8', - metrics: [ - { - name: 'A', - aggregation: 'sum', - field: 'host.network.egress.bytes', - }, - ], - }, - ], }); diff --git a/x-pack/plugins/entity_manager/server/lib/entities/built_in/services_from_ecs_data.ts b/x-pack/plugins/entity_manager/server/lib/entities/built_in/services_from_ecs_data.ts index c1496f424d393..d6aa4d08ad221 100644 --- a/x-pack/plugins/entity_manager/server/lib/entities/built_in/services_from_ecs_data.ts +++ b/x-pack/plugins/entity_manager/server/lib/entities/built_in/services_from_ecs_data.ts @@ -8,36 +8,20 @@ import { EntityDefinition, entityDefinitionSchema } from '@kbn/entities-schema'; import { BUILT_IN_ID_PREFIX } from './constants'; -const serviceTransactionFilter = (additionalFilters: string[] = []) => { - const baseFilters = [ - 'processor.event: "metric"', - 'metricset.name: "service_transaction"', - 'metricset.interval: "1m"', - ]; - - return [...baseFilters, ...additionalFilters].join(' AND '); -}; - export const builtInServicesFromEcsEntityDefinition: EntityDefinition = entityDefinitionSchema.parse({ - version: '0.3.0', + version: '0.4.0', id: `${BUILT_IN_ID_PREFIX}services_from_ecs_data`, name: 'Services from ECS data', description: 'This definition extracts service entities from common data streams by looking for the ECS field service.name', type: 'service', managed: true, - indexPatterns: [ - 'logs-*', - 'filebeat*', - 'metrics-apm.service_transaction.1m*', - 'metrics-apm.service_summary.1m*', - ], - history: { + indexPatterns: ['logs-*', 'filebeat*', 'traces-apm*'], + latest: { timestampField: '@timestamp', - interval: '1m', + lookbackPeriod: '10m', settings: { - lookbackPeriod: '10m', frequency: '2m', syncDelay: '2m', }, @@ -65,72 +49,9 @@ export const builtInServicesFromEcsEntityDefinition: EntityDefinition = 'cloud.provider', 'cloud.availability_zone', 'cloud.machine.type', - ], - metrics: [ - { - name: 'latency', - equation: 'A', - metrics: [ - { - name: 'A', - aggregation: 'avg', - filter: serviceTransactionFilter(), - field: 'transaction.duration.histogram', - }, - ], - }, - { - name: 'throughput', - equation: 'A', - metrics: [ - { - name: 'A', - aggregation: 'value_count', - filter: serviceTransactionFilter(), - field: 'transaction.duration.summary', - }, - ], - }, - { - name: 'failedTransactionRate', - equation: '1 - (A / B)', - metrics: [ - { - name: 'A', - aggregation: 'sum', - filter: serviceTransactionFilter(), - field: 'event.success_count', - }, - { - name: 'B', - aggregation: 'value_count', - filter: serviceTransactionFilter(), - field: 'event.success_count', - }, - ], - }, - { - name: 'logErrorRate', - equation: 'A', - metrics: [ - { - name: 'A', - aggregation: 'doc_count', - filter: - 'log.level: "error" OR log.level: "ERROR" OR error.log.level: "error" OR error.log.level: "ERROR"', - }, - ], - }, - { - name: 'logRate', - equation: 'A', - metrics: [ - { - name: 'A', - aggregation: 'doc_count', - filter: 'data_stream.type: logs', - }, - ], - }, + 'kubernetes.namespace', + 'orchestrator.cluster.name', + 'k8s.namespace.name', + 'k8s.cluster.name', ], }); diff --git a/x-pack/plugins/entity_manager/server/lib/entities/create_and_install_ingest_pipeline.ts b/x-pack/plugins/entity_manager/server/lib/entities/create_and_install_ingest_pipeline.ts index 360f416cd5a00..0b3900363c0c8 100644 --- a/x-pack/plugins/entity_manager/server/lib/entities/create_and_install_ingest_pipeline.ts +++ b/x-pack/plugins/entity_manager/server/lib/entities/create_and_install_ingest_pipeline.ts @@ -7,46 +7,15 @@ import { ElasticsearchClient, Logger } from '@kbn/core/server'; import { EntityDefinition } from '@kbn/entities-schema'; -import { - generateHistoryIngestPipelineId, - generateLatestIngestPipelineId, -} from './helpers/generate_component_id'; +import { generateLatestIngestPipelineId } from './helpers/generate_component_id'; import { retryTransientEsErrors } from './helpers/retry'; -import { generateHistoryProcessors } from './ingest_pipeline/generate_history_processors'; import { generateLatestProcessors } from './ingest_pipeline/generate_latest_processors'; -export async function createAndInstallHistoryIngestPipeline( +export async function createAndInstallIngestPipelines( esClient: ElasticsearchClient, definition: EntityDefinition, logger: Logger -) { - try { - const historyProcessors = generateHistoryProcessors(definition); - const historyId = generateHistoryIngestPipelineId(definition); - await retryTransientEsErrors( - () => - esClient.ingest.putPipeline({ - id: historyId, - processors: historyProcessors, - _meta: { - definitionVersion: definition.version, - managed: definition.managed, - }, - }), - { logger } - ); - } catch (e) { - logger.error( - `Cannot create entity history ingest pipelines for [${definition.id}] entity defintion` - ); - throw e; - } -} -export async function createAndInstallLatestIngestPipeline( - esClient: ElasticsearchClient, - definition: EntityDefinition, - logger: Logger -) { +): Promise> { try { const latestProcessors = generateLatestProcessors(definition); const latestId = generateLatestIngestPipelineId(definition); @@ -62,9 +31,10 @@ export async function createAndInstallLatestIngestPipeline( }), { logger } ); + return [{ type: 'ingest_pipeline', id: latestId }]; } catch (e) { logger.error( - `Cannot create entity latest ingest pipelines for [${definition.id}] entity defintion` + `Cannot create entity latest ingest pipelines for [${definition.id}] entity definition` ); throw e; } diff --git a/x-pack/plugins/entity_manager/server/lib/entities/create_and_install_transform.ts b/x-pack/plugins/entity_manager/server/lib/entities/create_and_install_transform.ts index d6379773479fc..779e0994a33b8 100644 --- a/x-pack/plugins/entity_manager/server/lib/entities/create_and_install_transform.ts +++ b/x-pack/plugins/entity_manager/server/lib/entities/create_and_install_transform.ts @@ -9,57 +9,20 @@ import { ElasticsearchClient, Logger } from '@kbn/core/server'; import { EntityDefinition } from '@kbn/entities-schema'; import { retryTransientEsErrors } from './helpers/retry'; import { generateLatestTransform } from './transform/generate_latest_transform'; -import { - generateBackfillHistoryTransform, - generateHistoryTransform, -} from './transform/generate_history_transform'; -export async function createAndInstallHistoryTransform( +export async function createAndInstallTransforms( esClient: ElasticsearchClient, definition: EntityDefinition, logger: Logger -) { - try { - const historyTransform = generateHistoryTransform(definition); - await retryTransientEsErrors(() => esClient.transform.putTransform(historyTransform), { - logger, - }); - } catch (e) { - logger.error(`Cannot create entity history transform for [${definition.id}] entity definition`); - throw e; - } -} - -export async function createAndInstallHistoryBackfillTransform( - esClient: ElasticsearchClient, - definition: EntityDefinition, - logger: Logger -) { - try { - const historyTransform = generateBackfillHistoryTransform(definition); - await retryTransientEsErrors(() => esClient.transform.putTransform(historyTransform), { - logger, - }); - } catch (e) { - logger.error( - `Cannot create entity history backfill transform for [${definition.id}] entity definition` - ); - throw e; - } -} - -export async function createAndInstallLatestTransform( - esClient: ElasticsearchClient, - definition: EntityDefinition, - logger: Logger -) { +): Promise> { try { const latestTransform = generateLatestTransform(definition); await retryTransientEsErrors(() => esClient.transform.putTransform(latestTransform), { logger, }); + return [{ type: 'transform', id: latestTransform.transform_id }]; } catch (e) { - logger.error(`Cannot create entity latest transform for [${definition.id}] entity definition`); + logger.error(`Cannot create entity history transform for [${definition.id}] entity definition`); throw e; } } diff --git a/x-pack/plugins/entity_manager/server/lib/entities/delete_ingest_pipeline.ts b/x-pack/plugins/entity_manager/server/lib/entities/delete_ingest_pipeline.ts index f4c46d8447d8f..a3b910dd4cb5e 100644 --- a/x-pack/plugins/entity_manager/server/lib/entities/delete_ingest_pipeline.ts +++ b/x-pack/plugins/entity_manager/server/lib/entities/delete_ingest_pipeline.ts @@ -7,24 +7,24 @@ import { ElasticsearchClient, Logger } from '@kbn/core/server'; import { EntityDefinition } from '@kbn/entities-schema'; -import { - generateHistoryIngestPipelineId, - generateLatestIngestPipelineId, -} from './helpers/generate_component_id'; import { retryTransientEsErrors } from './helpers/retry'; +import { generateLatestIngestPipelineId } from './helpers/generate_component_id'; -export async function deleteHistoryIngestPipeline( +export async function deleteIngestPipelines( esClient: ElasticsearchClient, definition: EntityDefinition, logger: Logger ) { try { - const historyPipelineId = generateHistoryIngestPipelineId(definition); - await retryTransientEsErrors(() => - esClient.ingest.deletePipeline({ id: historyPipelineId }, { ignore: [404] }) + await Promise.all( + (definition.installedComponents ?? []) + .filter(({ type }) => type === 'ingest_pipeline') + .map(({ id }) => + retryTransientEsErrors(() => esClient.ingest.deletePipeline({ id }, { ignore: [404] })) + ) ); } catch (e) { - logger.error(`Unable to delete history ingest pipeline [${definition.id}]: ${e}`); + logger.error(`Unable to delete ingest pipelines for definition [${definition.id}]: ${e}`); throw e; } } @@ -35,9 +35,11 @@ export async function deleteLatestIngestPipeline( logger: Logger ) { try { - const latestPipelineId = generateLatestIngestPipelineId(definition); await retryTransientEsErrors(() => - esClient.ingest.deletePipeline({ id: latestPipelineId }, { ignore: [404] }) + esClient.ingest.deletePipeline( + { id: generateLatestIngestPipelineId(definition) }, + { ignore: [404] } + ) ); } catch (e) { logger.error(`Unable to delete latest ingest pipeline [${definition.id}]: ${e}`); diff --git a/x-pack/plugins/entity_manager/server/lib/entities/delete_transforms.ts b/x-pack/plugins/entity_manager/server/lib/entities/delete_transforms.ts index a66c0998c014d..79b83998d38db 100644 --- a/x-pack/plugins/entity_manager/server/lib/entities/delete_transforms.ts +++ b/x-pack/plugins/entity_manager/server/lib/entities/delete_transforms.ts @@ -7,14 +7,8 @@ import { ElasticsearchClient, Logger } from '@kbn/core/server'; import { EntityDefinition } from '@kbn/entities-schema'; - -import { - generateHistoryTransformId, - generateHistoryBackfillTransformId, - generateLatestTransformId, -} from './helpers/generate_component_id'; import { retryTransientEsErrors } from './helpers/retry'; -import { isBackfillEnabled } from './helpers/is_backfill_enabled'; +import { generateLatestTransformId } from './helpers/generate_component_id'; export async function deleteTransforms( esClient: ElasticsearchClient, @@ -22,37 +16,42 @@ export async function deleteTransforms( logger: Logger ) { try { - const historyTransformId = generateHistoryTransformId(definition); - const latestTransformId = generateLatestTransformId(definition); - await retryTransientEsErrors( - () => - esClient.transform.deleteTransform( - { transform_id: historyTransformId, force: true }, - { ignore: [404] } - ), - { logger } + await Promise.all( + (definition.installedComponents ?? []) + .filter(({ type }) => type === 'transform') + .map(({ id }) => + retryTransientEsErrors( + () => + esClient.transform.deleteTransform( + { transform_id: id, force: true }, + { ignore: [404] } + ), + { logger } + ) + ) ); - if (isBackfillEnabled(definition)) { - const historyBackfillTransformId = generateHistoryBackfillTransformId(definition); - await retryTransientEsErrors( - () => - esClient.transform.deleteTransform( - { transform_id: historyBackfillTransformId, force: true }, - { ignore: [404] } - ), - { logger } - ); - } + } catch (e) { + logger.error(`Cannot delete transforms for definition [${definition.id}]: ${e}`); + throw e; + } +} + +export async function deleteLatestTransform( + esClient: ElasticsearchClient, + definition: EntityDefinition, + logger: Logger +) { + try { await retryTransientEsErrors( () => esClient.transform.deleteTransform( - { transform_id: latestTransformId, force: true }, + { transform_id: generateLatestTransformId(definition), force: true }, { ignore: [404] } ), { logger } ); } catch (e) { - logger.error(`Cannot delete history transform [${definition.id}]: ${e}`); + logger.error(`Cannot delete latest transform for definition [${definition.id}]: ${e}`); throw e; } } diff --git a/x-pack/plugins/entity_manager/server/lib/entities/find_entity_definition.ts b/x-pack/plugins/entity_manager/server/lib/entities/find_entity_definition.ts index d1d84f27414af..cfbb5a5ef5556 100644 --- a/x-pack/plugins/entity_manager/server/lib/entities/find_entity_definition.ts +++ b/x-pack/plugins/entity_manager/server/lib/entities/find_entity_definition.ts @@ -10,18 +10,8 @@ import { ElasticsearchClient, SavedObjectsClientContract } from '@kbn/core/serve import { EntityDefinition } from '@kbn/entities-schema'; import { NodesIngestTotal } from '@elastic/elasticsearch/lib/api/types'; import { SO_ENTITY_DEFINITION_TYPE } from '../../saved_objects'; -import { - generateHistoryTransformId, - generateHistoryBackfillTransformId, - generateHistoryIngestPipelineId, - generateHistoryIndexTemplateId, - generateLatestTransformId, - generateLatestIngestPipelineId, - generateLatestIndexTemplateId, -} from './helpers/generate_component_id'; import { BUILT_IN_ID_PREFIX } from './built_in'; import { EntityDefinitionState, EntityDefinitionWithState } from './types'; -import { isBackfillEnabled } from './helpers/is_backfill_enabled'; export async function findEntityDefinitions({ soClient, @@ -120,11 +110,9 @@ async function getTransformState({ definition: EntityDefinition; esClient: ElasticsearchClient; }) { - const transformIds = [ - generateHistoryTransformId(definition), - generateLatestTransformId(definition), - ...(isBackfillEnabled(definition) ? [generateHistoryBackfillTransformId(definition)] : []), - ]; + const transformIds = (definition.installedComponents ?? []) + .filter(({ type }) => type === 'transform') + .map(({ id }) => id); const transformStats = await Promise.all( transformIds.map((id) => esClient.transform.getTransformStats({ transform_id: id })) @@ -152,10 +140,10 @@ async function getIngestPipelineState({ definition: EntityDefinition; esClient: ElasticsearchClient; }) { - const ingestPipelineIds = [ - generateHistoryIngestPipelineId(definition), - generateLatestIngestPipelineId(definition), - ]; + const ingestPipelineIds = (definition.installedComponents ?? []) + .filter(({ type }) => type === 'ingest_pipeline') + .map(({ id }) => id); + const [ingestPipelines, ingestPipelinesStats] = await Promise.all([ esClient.ingest.getPipeline({ id: ingestPipelineIds.join(',') }, { ignore: [404] }), esClient.nodes.stats({ @@ -193,10 +181,9 @@ async function getIndexTemplatesState({ definition: EntityDefinition; esClient: ElasticsearchClient; }) { - const indexTemplatesIds = [ - generateLatestIndexTemplateId(definition), - generateHistoryIndexTemplateId(definition), - ]; + const indexTemplatesIds = (definition.installedComponents ?? []) + .filter(({ type }) => type === 'template') + .map(({ id }) => id); const templates = await Promise.all( indexTemplatesIds.map((id) => esClient.indices diff --git a/x-pack/plugins/entity_manager/server/lib/entities/helpers/calculate_offset.ts b/x-pack/plugins/entity_manager/server/lib/entities/helpers/calculate_offset.ts deleted file mode 100644 index 3eba710561abf..0000000000000 --- a/x-pack/plugins/entity_manager/server/lib/entities/helpers/calculate_offset.ts +++ /dev/null @@ -1,34 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { EntityDefinition } from '@kbn/entities-schema'; -import moment from 'moment'; -import { - ENTITY_DEFAULT_HISTORY_FREQUENCY, - ENTITY_DEFAULT_HISTORY_SYNC_DELAY, -} from '../../../../common/constants_entities'; - -const durationToSeconds = (dateMath: string) => { - const parts = dateMath.match(/(\d+)([m|s|h|d])/); - if (!parts) { - throw new Error(`Invalid date math supplied: ${dateMath}`); - } - const value = parseInt(parts[1], 10); - const unit = parts[2] as 'm' | 's' | 'h' | 'd'; - return moment.duration(value, unit).asSeconds(); -}; - -export function calculateOffset(definition: EntityDefinition) { - const syncDelay = durationToSeconds( - definition.history.settings.syncDelay || ENTITY_DEFAULT_HISTORY_SYNC_DELAY - ); - const frequency = - durationToSeconds(definition.history.settings.frequency || ENTITY_DEFAULT_HISTORY_FREQUENCY) * - 2; - - return syncDelay + frequency; -} diff --git a/x-pack/plugins/entity_manager/server/lib/entities/helpers/fixtures/builtin_entity_definition.ts b/x-pack/plugins/entity_manager/server/lib/entities/helpers/fixtures/builtin_entity_definition.ts index 5092e2caa5d78..b1e506150fb60 100644 --- a/x-pack/plugins/entity_manager/server/lib/entities/helpers/fixtures/builtin_entity_definition.ts +++ b/x-pack/plugins/entity_manager/server/lib/entities/helpers/fixtures/builtin_entity_definition.ts @@ -13,9 +13,8 @@ export const builtInEntityDefinition = entityDefinitionSchema.parse({ type: 'service', indexPatterns: ['kbn-data-forge-fake_stack.*'], managed: true, - history: { + latest: { timestampField: '@timestamp', - interval: '1m', }, identityFields: ['log.logger', { field: 'event.category', optional: true }], displayNameTemplate: '{{log.logger}}{{#event.category}}:{{.}}{{/event.category}}', diff --git a/x-pack/plugins/entity_manager/server/lib/entities/helpers/fixtures/entity_definition.ts b/x-pack/plugins/entity_manager/server/lib/entities/helpers/fixtures/entity_definition.ts index 940e209260c54..00ab9ac7759af 100644 --- a/x-pack/plugins/entity_manager/server/lib/entities/helpers/fixtures/entity_definition.ts +++ b/x-pack/plugins/entity_manager/server/lib/entities/helpers/fixtures/entity_definition.ts @@ -12,13 +12,12 @@ export const rawEntityDefinition = { name: 'Services for Admin Console', type: 'service', indexPatterns: ['kbn-data-forge-fake_stack.*'], - history: { + latest: { timestampField: '@timestamp', - interval: '1m', + lookbackPeriod: '10m', settings: { - lookbackPeriod: '10m', - frequency: '2m', - syncDelay: '2m', + frequency: '30s', + syncDelay: '10s', }, }, identityFields: ['log.logger', { field: 'event.category', optional: true }], diff --git a/x-pack/plugins/entity_manager/server/lib/entities/helpers/fixtures/entity_definition_with_backfill.ts b/x-pack/plugins/entity_manager/server/lib/entities/helpers/fixtures/entity_definition_with_backfill.ts deleted file mode 100644 index 66a79825fbfb0..0000000000000 --- a/x-pack/plugins/entity_manager/server/lib/entities/helpers/fixtures/entity_definition_with_backfill.ts +++ /dev/null @@ -1,51 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { entityDefinitionSchema } from '@kbn/entities-schema'; -export const entityDefinitionWithBackfill = entityDefinitionSchema.parse({ - id: 'admin-console-services-backfill', - version: '999.999.999', - name: 'Services for Admin Console', - type: 'service', - indexPatterns: ['kbn-data-forge-fake_stack.*'], - history: { - timestampField: '@timestamp', - interval: '1m', - settings: { - backfillSyncDelay: '15m', - backfillLookbackPeriod: '72h', - backfillFrequency: '5m', - }, - }, - identityFields: ['log.logger', { field: 'event.category', optional: true }], - displayNameTemplate: '{{log.logger}}{{#event.category}}:{{.}}{{/event.category}}', - metadata: ['tags', 'host.name', 'host.os.name', { source: '_index', destination: 'sourceIndex' }], - metrics: [ - { - name: 'logRate', - equation: 'A', - metrics: [ - { - name: 'A', - aggregation: 'doc_count', - filter: 'log.level: *', - }, - ], - }, - { - name: 'errorRate', - equation: 'A', - metrics: [ - { - name: 'A', - aggregation: 'doc_count', - filter: 'log.level: "ERROR"', - }, - ], - }, - ], -}); diff --git a/x-pack/plugins/entity_manager/server/lib/entities/helpers/fixtures/index.ts b/x-pack/plugins/entity_manager/server/lib/entities/helpers/fixtures/index.ts index c24dcee1f8cf7..e841b1c8e23dd 100644 --- a/x-pack/plugins/entity_manager/server/lib/entities/helpers/fixtures/index.ts +++ b/x-pack/plugins/entity_manager/server/lib/entities/helpers/fixtures/index.ts @@ -6,5 +6,4 @@ */ export { entityDefinition } from './entity_definition'; -export { entityDefinitionWithBackfill } from './entity_definition_with_backfill'; export { builtInEntityDefinition } from './builtin_entity_definition'; diff --git a/x-pack/plugins/entity_manager/server/lib/entities/ingest_pipeline/__snapshots__/generate_history_processors.test.ts.snap b/x-pack/plugins/entity_manager/server/lib/entities/ingest_pipeline/__snapshots__/generate_history_processors.test.ts.snap deleted file mode 100644 index c2e4605e5f909..0000000000000 --- a/x-pack/plugins/entity_manager/server/lib/entities/ingest_pipeline/__snapshots__/generate_history_processors.test.ts.snap +++ /dev/null @@ -1,327 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`generateHistoryProcessors(definition) should generate a valid pipeline for builtin definition 1`] = ` -Array [ - Object { - "set": Object { - "field": "event.ingested", - "value": "{{{_ingest.timestamp}}}", - }, - }, - Object { - "set": Object { - "field": "entity.type", - "value": "service", - }, - }, - Object { - "set": Object { - "field": "entity.definitionId", - "value": "builtin_mock_entity_definition", - }, - }, - Object { - "set": Object { - "field": "entity.definitionVersion", - "value": "1.0.0", - }, - }, - Object { - "set": Object { - "field": "entity.schemaVersion", - "value": "v1", - }, - }, - Object { - "set": Object { - "field": "entity.identityFields", - "value": Array [ - "log.logger", - "event.category", - ], - }, - }, - Object { - "script": Object { - "description": "Generated the entity.id field", - "source": "// This function will recursively collect all the values of a HashMap of HashMaps -Collection collectValues(HashMap subject) { - Collection values = new ArrayList(); - // Iterate through the values - for(Object value: subject.values()) { - // If the value is a HashMap, recurse - if (value instanceof HashMap) { - values.addAll(collectValues((HashMap) value)); - } else { - values.add(String.valueOf(value)); - } - } - return values; -} -// Create the string builder -StringBuilder entityId = new StringBuilder(); -if (ctx[\\"entity\\"][\\"identity\\"] != null) { - // Get the values as a collection - Collection values = collectValues(ctx[\\"entity\\"][\\"identity\\"]); - // Convert to a list and sort - List sortedValues = new ArrayList(values); - Collections.sort(sortedValues); - // Create comma delimited string - for(String instanceValue: sortedValues) { - entityId.append(instanceValue); - entityId.append(\\":\\"); - } - // Assign the entity.id - ctx[\\"entity\\"][\\"id\\"] = entityId.length() > 0 ? entityId.substring(0, entityId.length() - 1) : \\"unknown\\"; -}", - }, - }, - Object { - "fingerprint": Object { - "fields": Array [ - "entity.id", - ], - "method": "MurmurHash3", - "target_field": "entity.id", - }, - }, - Object { - "script": Object { - "source": "if (ctx.entity?.metadata?.tags != null) { - ctx.tags = ctx.entity.metadata.tags.keySet(); -} -if (ctx.entity?.metadata?.host?.name != null) { - if (ctx.host == null) { - ctx.host = new HashMap(); - } - ctx.host.name = ctx.entity.metadata.host.name.keySet(); -} -if (ctx.entity?.metadata?.host?.os?.name != null) { - if (ctx.host == null) { - ctx.host = new HashMap(); - } - if (ctx.host.os == null) { - ctx.host.os = new HashMap(); - } - ctx.host.os.name = ctx.entity.metadata.host.os.name.keySet(); -} -if (ctx.entity?.metadata?.sourceIndex != null) { - ctx.sourceIndex = ctx.entity.metadata.sourceIndex.keySet(); -}", - }, - }, - Object { - "remove": Object { - "field": "entity.metadata", - "ignore_missing": true, - }, - }, - Object { - "set": Object { - "field": "log.logger", - "if": "ctx.entity?.identity?.log?.logger != null", - "value": "{{entity.identity.log.logger}}", - }, - }, - Object { - "set": Object { - "field": "event.category", - "if": "ctx.entity?.identity?.event?.category != null", - "value": "{{entity.identity.event.category}}", - }, - }, - Object { - "remove": Object { - "field": "entity.identity", - "ignore_missing": true, - }, - }, - Object { - "date_index_name": Object { - "date_formats": Array [ - "UNIX_MS", - "ISO8601", - "yyyy-MM-dd'T'HH:mm:ss.SSSXX", - ], - "date_rounding": "M", - "field": "@timestamp", - "index_name_prefix": ".entities.v1.history.builtin_mock_entity_definition.", - }, - }, -] -`; - -exports[`generateHistoryProcessors(definition) should generate a valid pipeline for custom definition 1`] = ` -Array [ - Object { - "set": Object { - "field": "event.ingested", - "value": "{{{_ingest.timestamp}}}", - }, - }, - Object { - "set": Object { - "field": "entity.type", - "value": "service", - }, - }, - Object { - "set": Object { - "field": "entity.definitionId", - "value": "admin-console-services", - }, - }, - Object { - "set": Object { - "field": "entity.definitionVersion", - "value": "1.0.0", - }, - }, - Object { - "set": Object { - "field": "entity.schemaVersion", - "value": "v1", - }, - }, - Object { - "set": Object { - "field": "entity.identityFields", - "value": Array [ - "log.logger", - "event.category", - ], - }, - }, - Object { - "script": Object { - "description": "Generated the entity.id field", - "source": "// This function will recursively collect all the values of a HashMap of HashMaps -Collection collectValues(HashMap subject) { - Collection values = new ArrayList(); - // Iterate through the values - for(Object value: subject.values()) { - // If the value is a HashMap, recurse - if (value instanceof HashMap) { - values.addAll(collectValues((HashMap) value)); - } else { - values.add(String.valueOf(value)); - } - } - return values; -} -// Create the string builder -StringBuilder entityId = new StringBuilder(); -if (ctx[\\"entity\\"][\\"identity\\"] != null) { - // Get the values as a collection - Collection values = collectValues(ctx[\\"entity\\"][\\"identity\\"]); - // Convert to a list and sort - List sortedValues = new ArrayList(values); - Collections.sort(sortedValues); - // Create comma delimited string - for(String instanceValue: sortedValues) { - entityId.append(instanceValue); - entityId.append(\\":\\"); - } - // Assign the entity.id - ctx[\\"entity\\"][\\"id\\"] = entityId.length() > 0 ? entityId.substring(0, entityId.length() - 1) : \\"unknown\\"; -}", - }, - }, - Object { - "fingerprint": Object { - "fields": Array [ - "entity.id", - ], - "method": "MurmurHash3", - "target_field": "entity.id", - }, - }, - Object { - "script": Object { - "source": "if (ctx.entity?.metadata?.tags != null) { - ctx.tags = ctx.entity.metadata.tags.keySet(); -} -if (ctx.entity?.metadata?.host?.name != null) { - if (ctx.host == null) { - ctx.host = new HashMap(); - } - ctx.host.name = ctx.entity.metadata.host.name.keySet(); -} -if (ctx.entity?.metadata?.host?.os?.name != null) { - if (ctx.host == null) { - ctx.host = new HashMap(); - } - if (ctx.host.os == null) { - ctx.host.os = new HashMap(); - } - ctx.host.os.name = ctx.entity.metadata.host.os.name.keySet(); -} -if (ctx.entity?.metadata?.sourceIndex != null) { - ctx.sourceIndex = ctx.entity.metadata.sourceIndex.keySet(); -}", - }, - }, - Object { - "remove": Object { - "field": "entity.metadata", - "ignore_missing": true, - }, - }, - Object { - "set": Object { - "field": "log.logger", - "if": "ctx.entity?.identity?.log?.logger != null", - "value": "{{entity.identity.log.logger}}", - }, - }, - Object { - "set": Object { - "field": "event.category", - "if": "ctx.entity?.identity?.event?.category != null", - "value": "{{entity.identity.event.category}}", - }, - }, - Object { - "remove": Object { - "field": "entity.identity", - "ignore_missing": true, - }, - }, - Object { - "date_index_name": Object { - "date_formats": Array [ - "UNIX_MS", - "ISO8601", - "yyyy-MM-dd'T'HH:mm:ss.SSSXX", - ], - "date_rounding": "M", - "field": "@timestamp", - "index_name_prefix": ".entities.v1.history.admin-console-services.", - }, - }, - Object { - "pipeline": Object { - "ignore_missing_pipeline": true, - "name": "admin-console-services@platform", - }, - }, - Object { - "pipeline": Object { - "ignore_missing_pipeline": true, - "name": "admin-console-services-history@platform", - }, - }, - Object { - "pipeline": Object { - "ignore_missing_pipeline": true, - "name": "admin-console-services@custom", - }, - }, - Object { - "pipeline": Object { - "ignore_missing_pipeline": true, - "name": "admin-console-services-history@custom", - }, - }, -] -`; diff --git a/x-pack/plugins/entity_manager/server/lib/entities/ingest_pipeline/__snapshots__/generate_latest_processors.test.ts.snap b/x-pack/plugins/entity_manager/server/lib/entities/ingest_pipeline/__snapshots__/generate_latest_processors.test.ts.snap index f277b3ac84ab8..218deda422fe2 100644 --- a/x-pack/plugins/entity_manager/server/lib/entities/ingest_pipeline/__snapshots__/generate_latest_processors.test.ts.snap +++ b/x-pack/plugins/entity_manager/server/lib/entities/ingest_pipeline/__snapshots__/generate_latest_processors.test.ts.snap @@ -43,16 +43,60 @@ Array [ }, Object { "script": Object { - "source": "if (ctx.entity?.metadata?.tags.data != null) { + "description": "Generated the entity.id field", + "source": "// This function will recursively collect all the values of a HashMap of HashMaps +Collection collectValues(HashMap subject) { + Collection values = new ArrayList(); + // Iterate through the values + for(Object value: subject.values()) { + // If the value is a HashMap, recurse + if (value instanceof HashMap) { + values.addAll(collectValues((HashMap) value)); + } else { + values.add(String.valueOf(value)); + } + } + return values; +} +// Create the string builder +StringBuilder entityId = new StringBuilder(); +if (ctx[\\"entity\\"][\\"identity\\"] != null) { + // Get the values as a collection + Collection values = collectValues(ctx[\\"entity\\"][\\"identity\\"]); + // Convert to a list and sort + List sortedValues = new ArrayList(values); + Collections.sort(sortedValues); + // Create comma delimited string + for(String instanceValue: sortedValues) { + entityId.append(instanceValue); + entityId.append(\\":\\"); + } + // Assign the entity.id + ctx[\\"entity\\"][\\"id\\"] = entityId.length() > 0 ? entityId.substring(0, entityId.length() - 1) : \\"unknown\\"; +}", + }, + }, + Object { + "fingerprint": Object { + "fields": Array [ + "entity.id", + ], + "method": "MurmurHash3", + "target_field": "entity.id", + }, + }, + Object { + "script": Object { + "source": "if (ctx.entity?.metadata?.tags?.data != null) { ctx.tags = ctx.entity.metadata.tags.data.keySet(); } -if (ctx.entity?.metadata?.host?.name.data != null) { +if (ctx.entity?.metadata?.host?.name?.data != null) { if (ctx.host == null) { ctx.host = new HashMap(); } ctx.host.name = ctx.entity.metadata.host.name.data.keySet(); } -if (ctx.entity?.metadata?.host?.os?.name.data != null) { +if (ctx.entity?.metadata?.host?.os?.name?.data != null) { if (ctx.host == null) { ctx.host = new HashMap(); } @@ -61,7 +105,7 @@ if (ctx.entity?.metadata?.host?.os?.name.data != null) { } ctx.host.os.name = ctx.entity.metadata.host.os.name.data.keySet(); } -if (ctx.entity?.metadata?.sourceIndex.data != null) { +if (ctx.entity?.metadata?.sourceIndex?.data != null) { ctx.sourceIndex = ctx.entity.metadata.sourceIndex.data.keySet(); }", }, @@ -72,28 +116,18 @@ if (ctx.entity?.metadata?.sourceIndex.data != null) { "ignore_missing": true, }, }, - Object { - "dot_expander": Object { - "field": "log.logger", - "path": "entity.identity.log.logger.top_metric", - }, - }, Object { "set": Object { "field": "log.logger", - "value": "{{entity.identity.log.logger.top_metric.log.logger}}", - }, - }, - Object { - "dot_expander": Object { - "field": "event.category", - "path": "entity.identity.event.category.top_metric", + "if": "ctx.entity?.identity?.log?.logger != null", + "value": "{{entity.identity.log.logger}}", }, }, Object { "set": Object { "field": "event.category", - "value": "{{entity.identity.event.category.top_metric.event.category}}", + "if": "ctx.entity?.identity?.event?.category != null", + "value": "{{entity.identity.event.category}}", }, }, Object { @@ -160,16 +194,60 @@ Array [ }, Object { "script": Object { - "source": "if (ctx.entity?.metadata?.tags.data != null) { + "description": "Generated the entity.id field", + "source": "// This function will recursively collect all the values of a HashMap of HashMaps +Collection collectValues(HashMap subject) { + Collection values = new ArrayList(); + // Iterate through the values + for(Object value: subject.values()) { + // If the value is a HashMap, recurse + if (value instanceof HashMap) { + values.addAll(collectValues((HashMap) value)); + } else { + values.add(String.valueOf(value)); + } + } + return values; +} +// Create the string builder +StringBuilder entityId = new StringBuilder(); +if (ctx[\\"entity\\"][\\"identity\\"] != null) { + // Get the values as a collection + Collection values = collectValues(ctx[\\"entity\\"][\\"identity\\"]); + // Convert to a list and sort + List sortedValues = new ArrayList(values); + Collections.sort(sortedValues); + // Create comma delimited string + for(String instanceValue: sortedValues) { + entityId.append(instanceValue); + entityId.append(\\":\\"); + } + // Assign the entity.id + ctx[\\"entity\\"][\\"id\\"] = entityId.length() > 0 ? entityId.substring(0, entityId.length() - 1) : \\"unknown\\"; +}", + }, + }, + Object { + "fingerprint": Object { + "fields": Array [ + "entity.id", + ], + "method": "MurmurHash3", + "target_field": "entity.id", + }, + }, + Object { + "script": Object { + "source": "if (ctx.entity?.metadata?.tags?.data != null) { ctx.tags = ctx.entity.metadata.tags.data.keySet(); } -if (ctx.entity?.metadata?.host?.name.data != null) { +if (ctx.entity?.metadata?.host?.name?.data != null) { if (ctx.host == null) { ctx.host = new HashMap(); } ctx.host.name = ctx.entity.metadata.host.name.data.keySet(); } -if (ctx.entity?.metadata?.host?.os?.name.data != null) { +if (ctx.entity?.metadata?.host?.os?.name?.data != null) { if (ctx.host == null) { ctx.host = new HashMap(); } @@ -178,7 +256,7 @@ if (ctx.entity?.metadata?.host?.os?.name.data != null) { } ctx.host.os.name = ctx.entity.metadata.host.os.name.data.keySet(); } -if (ctx.entity?.metadata?.sourceIndex.data != null) { +if (ctx.entity?.metadata?.sourceIndex?.data != null) { ctx.sourceIndex = ctx.entity.metadata.sourceIndex.data.keySet(); }", }, @@ -189,28 +267,18 @@ if (ctx.entity?.metadata?.sourceIndex.data != null) { "ignore_missing": true, }, }, - Object { - "dot_expander": Object { - "field": "log.logger", - "path": "entity.identity.log.logger.top_metric", - }, - }, Object { "set": Object { "field": "log.logger", - "value": "{{entity.identity.log.logger.top_metric.log.logger}}", - }, - }, - Object { - "dot_expander": Object { - "field": "event.category", - "path": "entity.identity.event.category.top_metric", + "if": "ctx.entity?.identity?.log?.logger != null", + "value": "{{entity.identity.log.logger}}", }, }, Object { "set": Object { "field": "event.category", - "value": "{{entity.identity.event.category.top_metric.event.category}}", + "if": "ctx.entity?.identity?.event?.category != null", + "value": "{{entity.identity.event.category}}", }, }, Object { diff --git a/x-pack/plugins/entity_manager/server/lib/entities/ingest_pipeline/generate_history_processors.test.ts b/x-pack/plugins/entity_manager/server/lib/entities/ingest_pipeline/generate_history_processors.test.ts deleted file mode 100644 index 717241b89143d..0000000000000 --- a/x-pack/plugins/entity_manager/server/lib/entities/ingest_pipeline/generate_history_processors.test.ts +++ /dev/null @@ -1,21 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { entityDefinition, builtInEntityDefinition } from '../helpers/fixtures'; -import { generateHistoryProcessors } from './generate_history_processors'; - -describe('generateHistoryProcessors(definition)', () => { - it('should generate a valid pipeline for custom definition', () => { - const processors = generateHistoryProcessors(entityDefinition); - expect(processors).toMatchSnapshot(); - }); - - it('should generate a valid pipeline for builtin definition', () => { - const processors = generateHistoryProcessors(builtInEntityDefinition); - expect(processors).toMatchSnapshot(); - }); -}); diff --git a/x-pack/plugins/entity_manager/server/lib/entities/ingest_pipeline/generate_history_processors.ts b/x-pack/plugins/entity_manager/server/lib/entities/ingest_pipeline/generate_history_processors.ts deleted file mode 100644 index d51ab0be75db1..0000000000000 --- a/x-pack/plugins/entity_manager/server/lib/entities/ingest_pipeline/generate_history_processors.ts +++ /dev/null @@ -1,222 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { EntityDefinition, ENTITY_SCHEMA_VERSION_V1, MetadataField } from '@kbn/entities-schema'; -import { - initializePathScript, - cleanScript, -} from '../helpers/ingest_pipeline_script_processor_helpers'; -import { generateHistoryIndexName } from '../helpers/generate_component_id'; -import { isBuiltinDefinition } from '../helpers/is_builtin_definition'; - -function getMetadataSourceField({ aggregation, destination, source }: MetadataField) { - if (aggregation.type === 'terms') { - return `ctx.entity.metadata.${destination}.keySet()`; - } else if (aggregation.type === 'top_value') { - return `ctx.entity.metadata.${destination}.top_value["${source}"]`; - } -} - -function mapDestinationToPainless(metadata: MetadataField) { - const field = metadata.destination; - return ` - ${initializePathScript(field)} - ctx.${field} = ${getMetadataSourceField(metadata)}; - `; -} - -function createMetadataPainlessScript(definition: EntityDefinition) { - if (!definition.metadata) { - return ''; - } - - return definition.metadata.reduce((acc, metadata) => { - const { destination, source } = metadata; - const optionalFieldPath = destination.replaceAll('.', '?.'); - - if (metadata.aggregation.type === 'terms') { - const next = ` - if (ctx.entity?.metadata?.${optionalFieldPath} != null) { - ${mapDestinationToPainless(metadata)} - } - `; - return `${acc}\n${next}`; - } else if (metadata.aggregation.type === 'top_value') { - const next = ` - if (ctx.entity?.metadata?.${optionalFieldPath}?.top_value["${source}"] != null) { - ${mapDestinationToPainless(metadata)} - } - `; - return `${acc}\n${next}`; - } - - return acc; - }, ''); -} - -function liftIdentityFieldsToDocumentRoot(definition: EntityDefinition) { - return definition.identityFields.map((key) => ({ - set: { - if: `ctx.entity?.identity?.${key.field.replaceAll('.', '?.')} != null`, - field: key.field, - value: `{{entity.identity.${key.field}}}`, - }, - })); -} - -function getCustomIngestPipelines(definition: EntityDefinition) { - if (isBuiltinDefinition(definition)) { - return []; - } - - return [ - { - pipeline: { - ignore_missing_pipeline: true, - name: `${definition.id}@platform`, - }, - }, - { - pipeline: { - ignore_missing_pipeline: true, - name: `${definition.id}-history@platform`, - }, - }, - { - pipeline: { - ignore_missing_pipeline: true, - name: `${definition.id}@custom`, - }, - }, - { - pipeline: { - ignore_missing_pipeline: true, - name: `${definition.id}-history@custom`, - }, - }, - ]; -} - -export function generateHistoryProcessors(definition: EntityDefinition) { - return [ - { - set: { - field: 'event.ingested', - value: '{{{_ingest.timestamp}}}', - }, - }, - { - set: { - field: 'entity.type', - value: definition.type, - }, - }, - { - set: { - field: 'entity.definitionId', - value: definition.id, - }, - }, - { - set: { - field: 'entity.definitionVersion', - value: definition.version, - }, - }, - { - set: { - field: 'entity.schemaVersion', - value: ENTITY_SCHEMA_VERSION_V1, - }, - }, - { - set: { - field: 'entity.identityFields', - value: definition.identityFields.map((identityField) => identityField.field), - }, - }, - { - script: { - description: 'Generated the entity.id field', - source: cleanScript(` - // This function will recursively collect all the values of a HashMap of HashMaps - Collection collectValues(HashMap subject) { - Collection values = new ArrayList(); - // Iterate through the values - for(Object value: subject.values()) { - // If the value is a HashMap, recurse - if (value instanceof HashMap) { - values.addAll(collectValues((HashMap) value)); - } else { - values.add(String.valueOf(value)); - } - } - return values; - } - - // Create the string builder - StringBuilder entityId = new StringBuilder(); - - if (ctx["entity"]["identity"] != null) { - // Get the values as a collection - Collection values = collectValues(ctx["entity"]["identity"]); - - // Convert to a list and sort - List sortedValues = new ArrayList(values); - Collections.sort(sortedValues); - - // Create comma delimited string - for(String instanceValue: sortedValues) { - entityId.append(instanceValue); - entityId.append(":"); - } - - // Assign the entity.id - ctx["entity"]["id"] = entityId.length() > 0 ? entityId.substring(0, entityId.length() - 1) : "unknown"; - } - `), - }, - }, - { - fingerprint: { - fields: ['entity.id'], - target_field: 'entity.id', - method: 'MurmurHash3', - }, - }, - ...(definition.staticFields != null - ? Object.keys(definition.staticFields).map((field) => ({ - set: { field, value: definition.staticFields![field] }, - })) - : []), - ...(definition.metadata != null - ? [{ script: { source: cleanScript(createMetadataPainlessScript(definition)) } }] - : []), - { - remove: { - field: 'entity.metadata', - ignore_missing: true, - }, - }, - ...liftIdentityFieldsToDocumentRoot(definition), - { - remove: { - field: 'entity.identity', - ignore_missing: true, - }, - }, - { - date_index_name: { - field: '@timestamp', - index_name_prefix: `${generateHistoryIndexName(definition)}.`, - date_rounding: 'M', - date_formats: ['UNIX_MS', 'ISO8601', "yyyy-MM-dd'T'HH:mm:ss.SSSXX"], - }, - }, - ...getCustomIngestPipelines(definition), - ]; -} diff --git a/x-pack/plugins/entity_manager/server/lib/entities/ingest_pipeline/generate_latest_processors.ts b/x-pack/plugins/entity_manager/server/lib/entities/ingest_pipeline/generate_latest_processors.ts index 16823221fffb3..0e3812de2e320 100644 --- a/x-pack/plugins/entity_manager/server/lib/entities/ingest_pipeline/generate_latest_processors.ts +++ b/x-pack/plugins/entity_manager/server/lib/entities/ingest_pipeline/generate_latest_processors.ts @@ -17,7 +17,7 @@ function getMetadataSourceField({ aggregation, destination, source }: MetadataFi if (aggregation.type === 'terms') { return `ctx.entity.metadata.${destination}.data.keySet()`; } else if (aggregation.type === 'top_value') { - return `ctx.entity.metadata.${destination}.top_value["${destination}"]`; + return `ctx.entity.metadata.${destination}.top_value["${source}"]`; } } @@ -35,19 +35,19 @@ function createMetadataPainlessScript(definition: EntityDefinition) { } return definition.metadata.reduce((acc, metadata) => { - const destination = metadata.destination; + const { destination, source } = metadata; const optionalFieldPath = destination.replaceAll('.', '?.'); if (metadata.aggregation.type === 'terms') { const next = ` - if (ctx.entity?.metadata?.${optionalFieldPath}.data != null) { + if (ctx.entity?.metadata?.${optionalFieldPath}?.data != null) { ${mapDestinationToPainless(metadata)} } `; return `${acc}\n${next}`; } else if (metadata.aggregation.type === 'top_value') { const next = ` - if (ctx.entity?.metadata?.${optionalFieldPath}?.top_value["${destination}"] != null) { + if (ctx.entity?.metadata?.${optionalFieldPath}?.top_value["${source}"] != null) { ${mapDestinationToPainless(metadata)} } `; @@ -59,30 +59,13 @@ function createMetadataPainlessScript(definition: EntityDefinition) { } function liftIdentityFieldsToDocumentRoot(definition: EntityDefinition) { - return definition.identityFields - .map((identityField) => { - const setProcessor = { - set: { - field: identityField.field, - value: `{{entity.identity.${identityField.field}.top_metric.${identityField.field}}}`, - }, - }; - - if (!identityField.field.includes('.')) { - return [setProcessor]; - } - - return [ - { - dot_expander: { - field: identityField.field, - path: `entity.identity.${identityField.field}.top_metric`, - }, - }, - setProcessor, - ]; - }) - .flat(); + return definition.identityFields.map((key) => ({ + set: { + if: `ctx.entity?.identity?.${key.field.replaceAll('.', '?.')} != null`, + field: key.field, + value: `{{entity.identity.${key.field}}}`, + }, + })); } function getCustomIngestPipelines(definition: EntityDefinition) { @@ -156,6 +139,55 @@ export function generateLatestProcessors(definition: EntityDefinition) { value: definition.identityFields.map((identityField) => identityField.field), }, }, + { + script: { + description: 'Generated the entity.id field', + source: cleanScript(` + // This function will recursively collect all the values of a HashMap of HashMaps + Collection collectValues(HashMap subject) { + Collection values = new ArrayList(); + // Iterate through the values + for(Object value: subject.values()) { + // If the value is a HashMap, recurse + if (value instanceof HashMap) { + values.addAll(collectValues((HashMap) value)); + } else { + values.add(String.valueOf(value)); + } + } + return values; + } + + // Create the string builder + StringBuilder entityId = new StringBuilder(); + + if (ctx["entity"]["identity"] != null) { + // Get the values as a collection + Collection values = collectValues(ctx["entity"]["identity"]); + + // Convert to a list and sort + List sortedValues = new ArrayList(values); + Collections.sort(sortedValues); + + // Create comma delimited string + for(String instanceValue: sortedValues) { + entityId.append(instanceValue); + entityId.append(":"); + } + + // Assign the entity.id + ctx["entity"]["id"] = entityId.length() > 0 ? entityId.substring(0, entityId.length() - 1) : "unknown"; + } + `), + }, + }, + { + fingerprint: { + fields: ['entity.id'], + target_field: 'entity.id', + method: 'MurmurHash3', + }, + }, ...(definition.staticFields != null ? Object.keys(definition.staticFields).map((field) => ({ set: { field, value: definition.staticFields![field] }, @@ -177,8 +209,8 @@ export function generateLatestProcessors(definition: EntityDefinition) { ignore_missing: true, }, }, + // This must happen AFTER we lift the identity fields into the root of the document { - // This must happen AFTER we lift the identity fields into the root of the document set: { field: 'entity.displayName', value: definition.displayNameTemplate, diff --git a/x-pack/plugins/entity_manager/server/lib/entities/install_entity_definition.test.ts b/x-pack/plugins/entity_manager/server/lib/entities/install_entity_definition.test.ts index 5cee21dc43a07..e07670c58fd9b 100644 --- a/x-pack/plugins/entity_manager/server/lib/entities/install_entity_definition.test.ts +++ b/x-pack/plugins/entity_manager/server/lib/entities/install_entity_definition.test.ts @@ -19,19 +19,23 @@ import { } from './install_entity_definition'; import { SO_ENTITY_DEFINITION_TYPE } from '../../saved_objects'; import { - generateHistoryIndexTemplateId, - generateHistoryIngestPipelineId, - generateHistoryTransformId, generateLatestIndexTemplateId, generateLatestIngestPipelineId, generateLatestTransformId, } from './helpers/generate_component_id'; -import { generateHistoryTransform } from './transform/generate_history_transform'; import { generateLatestTransform } from './transform/generate_latest_transform'; import { entityDefinition as mockEntityDefinition } from './helpers/fixtures/entity_definition'; import { EntityDefinitionIdInvalid } from './errors/entity_definition_id_invalid'; import { EntityIdConflict } from './errors/entity_id_conflict_error'; +const getExpectedInstalledComponents = (definition: EntityDefinition) => { + return [ + { type: 'template', id: generateLatestIndexTemplateId(definition) }, + { type: 'ingest_pipeline', id: generateLatestIngestPipelineId(definition) }, + { type: 'transform', id: generateLatestTransformId(definition) }, + ]; +}; + const assertHasCreatedDefinition = ( definition: EntityDefinition, soClient: SavedObjectsClientContract, @@ -44,6 +48,7 @@ const assertHasCreatedDefinition = ( ...definition, installStatus: 'installing', installStartedAt: expect.any(String), + installedComponents: [], }, { id: definition.id, @@ -54,29 +59,17 @@ const assertHasCreatedDefinition = ( expect(soClient.update).toBeCalledTimes(1); expect(soClient.update).toBeCalledWith(SO_ENTITY_DEFINITION_TYPE, definition.id, { installStatus: 'installed', + installedComponents: getExpectedInstalledComponents(definition), }); - expect(esClient.indices.putIndexTemplate).toBeCalledTimes(2); - expect(esClient.indices.putIndexTemplate).toBeCalledWith( - expect.objectContaining({ - name: `entities_v1_history_${definition.id}_index_template`, - }) - ); + expect(esClient.indices.putIndexTemplate).toBeCalledTimes(1); expect(esClient.indices.putIndexTemplate).toBeCalledWith( expect.objectContaining({ name: `entities_v1_latest_${definition.id}_index_template`, }) ); - expect(esClient.ingest.putPipeline).toBeCalledTimes(2); - expect(esClient.ingest.putPipeline).toBeCalledWith({ - id: generateHistoryIngestPipelineId(definition), - processors: expect.anything(), - _meta: { - definitionVersion: definition.version, - managed: definition.managed, - }, - }); + expect(esClient.ingest.putPipeline).toBeCalledTimes(1); expect(esClient.ingest.putPipeline).toBeCalledWith({ id: generateLatestIngestPipelineId(definition), processors: expect.anything(), @@ -86,8 +79,7 @@ const assertHasCreatedDefinition = ( }, }); - expect(esClient.transform.putTransform).toBeCalledTimes(2); - expect(esClient.transform.putTransform).toBeCalledWith(generateHistoryTransform(definition)); + expect(esClient.transform.putTransform).toBeCalledTimes(1); expect(esClient.transform.putTransform).toBeCalledWith(generateLatestTransform(definition)); }; @@ -101,32 +93,21 @@ const assertHasUpgradedDefinition = ( ...definition, installStatus: 'upgrading', installStartedAt: expect.any(String), + installedComponents: getExpectedInstalledComponents(definition), }); expect(soClient.update).toBeCalledWith(SO_ENTITY_DEFINITION_TYPE, definition.id, { installStatus: 'installed', + installedComponents: getExpectedInstalledComponents(definition), }); - expect(esClient.indices.putIndexTemplate).toBeCalledTimes(2); - expect(esClient.indices.putIndexTemplate).toBeCalledWith( - expect.objectContaining({ - name: `entities_v1_history_${definition.id}_index_template`, - }) - ); + expect(esClient.indices.putIndexTemplate).toBeCalledTimes(1); expect(esClient.indices.putIndexTemplate).toBeCalledWith( expect.objectContaining({ name: `entities_v1_latest_${definition.id}_index_template`, }) ); - expect(esClient.ingest.putPipeline).toBeCalledTimes(2); - expect(esClient.ingest.putPipeline).toBeCalledWith({ - id: generateHistoryIngestPipelineId(definition), - processors: expect.anything(), - _meta: { - definitionVersion: definition.version, - managed: definition.managed, - }, - }); + expect(esClient.ingest.putPipeline).toBeCalledTimes(1); expect(esClient.ingest.putPipeline).toBeCalledWith({ id: generateLatestIngestPipelineId(definition), processors: expect.anything(), @@ -136,8 +117,7 @@ const assertHasUpgradedDefinition = ( }, }); - expect(esClient.transform.putTransform).toBeCalledTimes(2); - expect(esClient.transform.putTransform).toBeCalledWith(generateHistoryTransform(definition)); + expect(esClient.transform.putTransform).toBeCalledTimes(1); expect(esClient.transform.putTransform).toBeCalledWith(generateLatestTransform(definition)); }; @@ -148,13 +128,7 @@ const assertHasDeletedDefinition = ( ) => { assertHasDeletedTransforms(definition, esClient); - expect(esClient.ingest.deletePipeline).toBeCalledTimes(2); - expect(esClient.ingest.deletePipeline).toBeCalledWith( - { - id: generateHistoryIngestPipelineId(definition), - }, - { ignore: [404] } - ); + expect(esClient.ingest.deletePipeline).toBeCalledTimes(1); expect(esClient.ingest.deletePipeline).toBeCalledWith( { id: generateLatestIngestPipelineId(definition), @@ -162,13 +136,7 @@ const assertHasDeletedDefinition = ( { ignore: [404] } ); - expect(esClient.indices.deleteIndexTemplate).toBeCalledTimes(2); - expect(esClient.indices.deleteIndexTemplate).toBeCalledWith( - { - name: generateHistoryIndexTemplateId(definition), - }, - { ignore: [404] } - ); + expect(esClient.indices.deleteIndexTemplate).toBeCalledTimes(1); expect(esClient.indices.deleteIndexTemplate).toBeCalledWith( { name: generateLatestIndexTemplateId(definition), @@ -184,33 +152,21 @@ const assertHasDeletedTransforms = ( definition: EntityDefinition, esClient: ElasticsearchClient ) => { - expect(esClient.transform.stopTransform).toBeCalledTimes(2); - expect(esClient.transform.stopTransform).toBeCalledWith( - expect.objectContaining({ - transform_id: generateHistoryTransformId(definition), - }), - expect.anything() - ); - expect(esClient.transform.deleteTransform).toBeCalledWith( - expect.objectContaining({ - transform_id: generateHistoryTransformId(definition), - }), - expect.anything() - ); + expect(esClient.transform.stopTransform).toBeCalledTimes(1); expect(esClient.transform.stopTransform).toBeCalledWith( expect.objectContaining({ transform_id: generateLatestTransformId(definition), }), expect.anything() ); + + expect(esClient.transform.deleteTransform).toBeCalledTimes(1); expect(esClient.transform.deleteTransform).toBeCalledWith( expect.objectContaining({ transform_id: generateLatestTransformId(definition), }), expect.anything() ); - - expect(esClient.transform.deleteTransform).toBeCalledTimes(2); }; describe('install_entity_definition', () => { @@ -223,7 +179,7 @@ describe('install_entity_definition', () => { installEntityDefinition({ esClient, soClient, - definition: { id: 'a'.repeat(40) } as EntityDefinition, + definition: { id: 'a'.repeat(50) } as EntityDefinition, logger: loggerMock.create(), }) ).rejects.toThrow(EntityDefinitionIdInvalid); @@ -242,6 +198,7 @@ describe('install_entity_definition', () => { attributes: { ...mockEntityDefinition, installStatus: 'installed', + installedComponents: [], }, }, ], @@ -264,6 +221,12 @@ describe('install_entity_definition', () => { const esClient = elasticsearchClientMock.createScopedClusterClient().asCurrentUser; const soClient = savedObjectsClientMock.create(); soClient.find.mockResolvedValue({ saved_objects: [], total: 0, page: 1, per_page: 10 }); + soClient.update.mockResolvedValue({ + id: mockEntityDefinition.id, + type: 'entity-definition', + references: [], + attributes: {}, + }); await installEntityDefinition({ esClient, @@ -300,6 +263,12 @@ describe('install_entity_definition', () => { const esClient = elasticsearchClientMock.createScopedClusterClient().asCurrentUser; const soClient = savedObjectsClientMock.create(); soClient.find.mockResolvedValue({ saved_objects: [], total: 0, page: 1, per_page: 10 }); + soClient.update.mockResolvedValue({ + id: mockEntityDefinition.id, + type: 'entity-definition', + references: [], + attributes: {}, + }); await installBuiltInEntityDefinitions({ esClient, @@ -329,6 +298,7 @@ describe('install_entity_definition', () => { attributes: { ...mockEntityDefinition, installStatus: 'installed', + installedComponents: getExpectedInstalledComponents(mockEntityDefinition), }, }, ], @@ -336,6 +306,12 @@ describe('install_entity_definition', () => { page: 1, per_page: 10, }); + soClient.update.mockResolvedValue({ + id: mockEntityDefinition.id, + type: 'entity-definition', + references: [], + attributes: {}, + }); await installBuiltInEntityDefinitions({ esClient, @@ -367,6 +343,7 @@ describe('install_entity_definition', () => { attributes: { ...mockEntityDefinition, installStatus: 'installed', + installedComponents: getExpectedInstalledComponents(mockEntityDefinition), }, }, ], @@ -374,6 +351,12 @@ describe('install_entity_definition', () => { page: 1, per_page: 10, }); + soClient.update.mockResolvedValue({ + id: mockEntityDefinition.id, + type: 'entity-definition', + references: [], + attributes: {}, + }); await installBuiltInEntityDefinitions({ esClient, @@ -407,6 +390,7 @@ describe('install_entity_definition', () => { // upgrading for 1h installStatus: 'upgrading', installStartedAt: moment().subtract(1, 'hour').toISOString(), + installedComponents: getExpectedInstalledComponents(mockEntityDefinition), }, }, ], @@ -414,6 +398,12 @@ describe('install_entity_definition', () => { page: 1, per_page: 10, }); + soClient.update.mockResolvedValue({ + id: mockEntityDefinition.id, + type: 'entity-definition', + references: [], + attributes: {}, + }); await installBuiltInEntityDefinitions({ esClient, @@ -442,6 +432,7 @@ describe('install_entity_definition', () => { ...mockEntityDefinition, installStatus: 'failed', installStartedAt: new Date().toISOString(), + installedComponents: getExpectedInstalledComponents(mockEntityDefinition), }, }, ], @@ -449,6 +440,12 @@ describe('install_entity_definition', () => { page: 1, per_page: 10, }); + soClient.update.mockResolvedValue({ + id: mockEntityDefinition.id, + type: 'entity-definition', + references: [], + attributes: {}, + }); await installBuiltInEntityDefinitions({ esClient, diff --git a/x-pack/plugins/entity_manager/server/lib/entities/install_entity_definition.ts b/x-pack/plugins/entity_manager/server/lib/entities/install_entity_definition.ts index 7d6dee4fb2ced..b4adedaf10374 100644 --- a/x-pack/plugins/entity_manager/server/lib/entities/install_entity_definition.ts +++ b/x-pack/plugins/entity_manager/server/lib/entities/install_entity_definition.ts @@ -10,39 +10,25 @@ import { ElasticsearchClient } from '@kbn/core-elasticsearch-server'; import { SavedObjectsClientContract } from '@kbn/core-saved-objects-api-server'; import { EntityDefinition, EntityDefinitionUpdate } from '@kbn/entities-schema'; import { Logger } from '@kbn/logging'; -import { - generateHistoryIndexTemplateId, - generateLatestIndexTemplateId, -} from './helpers/generate_component_id'; -import { - createAndInstallHistoryIngestPipeline, - createAndInstallLatestIngestPipeline, -} from './create_and_install_ingest_pipeline'; -import { - createAndInstallHistoryBackfillTransform, - createAndInstallHistoryTransform, - createAndInstallLatestTransform, -} from './create_and_install_transform'; +import { generateLatestIndexTemplateId } from './helpers/generate_component_id'; +import { createAndInstallIngestPipelines } from './create_and_install_ingest_pipeline'; +import { createAndInstallTransforms } from './create_and_install_transform'; import { validateDefinitionCanCreateValidTransformIds } from './transform/validate_transform_ids'; import { deleteEntityDefinition } from './delete_entity_definition'; -import { deleteHistoryIngestPipeline, deleteLatestIngestPipeline } from './delete_ingest_pipeline'; +import { deleteLatestIngestPipeline } from './delete_ingest_pipeline'; import { findEntityDefinitionById } from './find_entity_definition'; import { entityDefinitionExists, saveEntityDefinition, updateEntityDefinition, } from './save_entity_definition'; - -import { isBackfillEnabled } from './helpers/is_backfill_enabled'; -import { deleteTemplate, upsertTemplate } from '../manage_index_templates'; -import { generateEntitiesLatestIndexTemplateConfig } from './templates/entities_latest_template'; -import { generateEntitiesHistoryIndexTemplateConfig } from './templates/entities_history_template'; +import { createAndInstallTemplates, deleteTemplate } from '../manage_index_templates'; import { EntityIdConflict } from './errors/entity_id_conflict_error'; import { EntityDefinitionNotFound } from './errors/entity_not_found'; import { mergeEntityDefinitionUpdate } from './helpers/merge_definition_update'; import { EntityDefinitionWithState } from './types'; -import { stopTransforms } from './stop_transforms'; -import { deleteTransforms } from './delete_transforms'; +import { stopLatestTransform, stopTransforms } from './stop_transforms'; +import { deleteLatestTransform, deleteTransforms } from './delete_transforms'; export interface InstallDefinitionParams { esClient: ElasticsearchClient; @@ -51,16 +37,6 @@ export interface InstallDefinitionParams { logger: Logger; } -const throwIfRejected = (values: Array | PromiseRejectedResult>) => { - const rejectedPromise = values.find( - (value) => value.status === 'rejected' - ) as PromiseRejectedResult; - if (rejectedPromise) { - throw new Error(rejectedPromise.reason); - } - return values; -}; - // install an entity definition from scratch with all its required components // after verifying that the definition id is valid and available. // attempt to remove all installed components if the installation fails. @@ -72,42 +48,35 @@ export async function installEntityDefinition({ }: InstallDefinitionParams): Promise { validateDefinitionCanCreateValidTransformIds(definition); - try { - if (await entityDefinitionExists(soClient, definition.id)) { - throw new EntityIdConflict( - `Entity definition with [${definition.id}] already exists.`, - definition - ); - } + if (await entityDefinitionExists(soClient, definition.id)) { + throw new EntityIdConflict( + `Entity definition with [${definition.id}] already exists.`, + definition + ); + } + try { const entityDefinition = await saveEntityDefinition(soClient, { ...definition, installStatus: 'installing', installStartedAt: new Date().toISOString(), + installedComponents: [], }); return await install({ esClient, soClient, logger, definition: entityDefinition }); } catch (e) { logger.error(`Failed to install entity definition ${definition.id}: ${e}`); - await stopAndDeleteTransforms(esClient, definition, logger); - await Promise.all([ - deleteHistoryIngestPipeline(esClient, definition, logger), - deleteLatestIngestPipeline(esClient, definition, logger), - ]); + await stopLatestTransform(esClient, definition, logger); + await deleteLatestTransform(esClient, definition, logger); - await Promise.all([ - deleteTemplate({ - esClient, - logger, - name: generateHistoryIndexTemplateId(definition), - }), - deleteTemplate({ - esClient, - logger, - name: generateLatestIndexTemplateId(definition), - }), - ]); + await deleteLatestIngestPipeline(esClient, definition, logger); + + await deleteTemplate({ + esClient, + logger, + name: generateLatestIndexTemplateId(definition), + }); await deleteEntityDefinition(soClient, definition).catch((err) => { if (err instanceof EntityDefinitionNotFound) { @@ -191,36 +160,19 @@ async function install({ ); logger.debug(`Installing index templates for definition ${definition.id}`); - await Promise.allSettled([ - upsertTemplate({ - esClient, - logger, - template: generateEntitiesHistoryIndexTemplateConfig(definition), - }), - upsertTemplate({ - esClient, - logger, - template: generateEntitiesLatestIndexTemplateConfig(definition), - }), - ]).then(throwIfRejected); + const templates = await createAndInstallTemplates(esClient, definition, logger); logger.debug(`Installing ingest pipelines for definition ${definition.id}`); - await Promise.allSettled([ - createAndInstallHistoryIngestPipeline(esClient, definition, logger), - createAndInstallLatestIngestPipeline(esClient, definition, logger), - ]).then(throwIfRejected); + const pipelines = await createAndInstallIngestPipelines(esClient, definition, logger); logger.debug(`Installing transforms for definition ${definition.id}`); - await Promise.allSettled([ - createAndInstallHistoryTransform(esClient, definition, logger), - isBackfillEnabled(definition) - ? createAndInstallHistoryBackfillTransform(esClient, definition, logger) - : Promise.resolve(), - createAndInstallLatestTransform(esClient, definition, logger), - ]).then(throwIfRejected); - - await updateEntityDefinition(soClient, definition.id, { installStatus: 'installed' }); - return { ...definition, installStatus: 'installed' }; + const transforms = await createAndInstallTransforms(esClient, definition, logger); + + const updatedProps = await updateEntityDefinition(soClient, definition.id, { + installStatus: 'installed', + installedComponents: [...templates, ...pipelines, ...transforms], + }); + return { ...definition, ...updatedProps.attributes }; } // stop and delete the current transforms and reinstall all the components diff --git a/x-pack/plugins/entity_manager/server/lib/entities/save_entity_definition.ts b/x-pack/plugins/entity_manager/server/lib/entities/save_entity_definition.ts index 2dff5178aeeaf..d32edfa146917 100644 --- a/x-pack/plugins/entity_manager/server/lib/entities/save_entity_definition.ts +++ b/x-pack/plugins/entity_manager/server/lib/entities/save_entity_definition.ts @@ -41,5 +41,5 @@ export async function updateEntityDefinition( id: string, definition: Partial ) { - await soClient.update(SO_ENTITY_DEFINITION_TYPE, id, definition); + return await soClient.update(SO_ENTITY_DEFINITION_TYPE, id, definition); } diff --git a/x-pack/plugins/entity_manager/server/lib/entities/start_transforms.ts b/x-pack/plugins/entity_manager/server/lib/entities/start_transforms.ts index ea2ec7adb5ddc..f4cd8fc89dd11 100644 --- a/x-pack/plugins/entity_manager/server/lib/entities/start_transforms.ts +++ b/x-pack/plugins/entity_manager/server/lib/entities/start_transforms.ts @@ -7,13 +7,7 @@ import { ElasticsearchClient, Logger } from '@kbn/core/server'; import { EntityDefinition } from '@kbn/entities-schema'; -import { - generateHistoryBackfillTransformId, - generateHistoryTransformId, - generateLatestTransformId, -} from './helpers/generate_component_id'; import { retryTransientEsErrors } from './helpers/retry'; -import { isBackfillEnabled } from './helpers/is_backfill_enabled'; export async function startTransforms( esClient: ElasticsearchClient, @@ -21,28 +15,15 @@ export async function startTransforms( logger: Logger ) { try { - const historyTransformId = generateHistoryTransformId(definition); - const latestTransformId = generateLatestTransformId(definition); - await retryTransientEsErrors( - () => - esClient.transform.startTransform({ transform_id: historyTransformId }, { ignore: [409] }), - { logger } - ); - if (isBackfillEnabled(definition)) { - const historyBackfillTransformId = generateHistoryBackfillTransformId(definition); - await retryTransientEsErrors( - () => - esClient.transform.startTransform( - { transform_id: historyBackfillTransformId }, - { ignore: [409] } - ), - { logger } - ); - } - await retryTransientEsErrors( - () => - esClient.transform.startTransform({ transform_id: latestTransformId }, { ignore: [409] }), - { logger } + await Promise.all( + (definition.installedComponents ?? []) + .filter(({ type }) => type === 'transform') + .map(({ id }) => + retryTransientEsErrors( + () => esClient.transform.startTransform({ transform_id: id }, { ignore: [409] }), + { logger } + ) + ) ); } catch (err) { logger.error(`Cannot start entity transforms [${definition.id}]: ${err}`); diff --git a/x-pack/plugins/entity_manager/server/lib/entities/stop_transforms.ts b/x-pack/plugins/entity_manager/server/lib/entities/stop_transforms.ts index 98f9ad351e377..9aabad926b239 100644 --- a/x-pack/plugins/entity_manager/server/lib/entities/stop_transforms.ts +++ b/x-pack/plugins/entity_manager/server/lib/entities/stop_transforms.ts @@ -8,14 +8,8 @@ import { ElasticsearchClient, Logger } from '@kbn/core/server'; import { EntityDefinition } from '@kbn/entities-schema'; -import { - generateHistoryTransformId, - generateHistoryBackfillTransformId, - generateLatestTransformId, -} from './helpers/generate_component_id'; import { retryTransientEsErrors } from './helpers/retry'; - -import { isBackfillEnabled } from './helpers/is_backfill_enabled'; +import { generateLatestTransformId } from './helpers/generate_component_id'; export async function stopTransforms( esClient: ElasticsearchClient, @@ -23,43 +17,46 @@ export async function stopTransforms( logger: Logger ) { try { - const historyTransformId = generateHistoryTransformId(definition); - const latestTransformId = generateLatestTransformId(definition); - - await retryTransientEsErrors( - () => - esClient.transform.stopTransform( - { transform_id: historyTransformId, wait_for_completion: true, force: true }, - { ignore: [409, 404] } - ), - { logger } + await Promise.all( + (definition.installedComponents ?? []) + .filter(({ type }) => type === 'transform') + .map(({ id }) => + retryTransientEsErrors( + () => + esClient.transform.stopTransform( + { transform_id: id, wait_for_completion: true, force: true }, + { ignore: [409, 404] } + ), + { logger } + ) + ) ); + } catch (e) { + logger.error(`Cannot stop transforms for definition [${definition.id}]: ${e}`); + throw e; + } +} - if (isBackfillEnabled(definition)) { - const historyBackfillTransformId = generateHistoryBackfillTransformId(definition); - await retryTransientEsErrors( - () => - esClient.transform.stopTransform( - { - transform_id: historyBackfillTransformId, - wait_for_completion: true, - force: true, - }, - { ignore: [409, 404] } - ), - { logger } - ); - } +export async function stopLatestTransform( + esClient: ElasticsearchClient, + definition: EntityDefinition, + logger: Logger +) { + try { await retryTransientEsErrors( () => esClient.transform.stopTransform( - { transform_id: latestTransformId, wait_for_completion: true, force: true }, + { + transform_id: generateLatestTransformId(definition), + wait_for_completion: true, + force: true, + }, { ignore: [409, 404] } ), { logger } ); } catch (e) { - logger.error(`Cannot stop entity transforms [${definition.id}]: ${e}`); + logger.error(`Cannot stop latest transform for definition [${definition.id}]: ${e}`); throw e; } } diff --git a/x-pack/plugins/entity_manager/server/lib/entities/templates/__snapshots__/entities_history_template.test.ts.snap b/x-pack/plugins/entity_manager/server/lib/entities/templates/__snapshots__/entities_history_template.test.ts.snap deleted file mode 100644 index fd4ed11f8cb94..0000000000000 --- a/x-pack/plugins/entity_manager/server/lib/entities/templates/__snapshots__/entities_history_template.test.ts.snap +++ /dev/null @@ -1,152 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`generateEntitiesHistoryIndexTemplateConfig(definition) should generate a valid index template for builtin definition 1`] = ` -Object { - "_meta": Object { - "description": "Index template for indices managed by the Elastic Entity Model's entity discovery framework for the history dataset", - "ecs_version": "8.0.0", - "managed": true, - "managed_by": "elastic_entity_model", - }, - "composed_of": Array [ - "entities_v1_history_base", - "entities_v1_entity", - "entities_v1_event", - ], - "ignore_missing_component_templates": Array [], - "index_patterns": Array [ - ".entities.v1.history.builtin_mock_entity_definition.*", - ], - "name": "entities_v1_history_builtin_mock_entity_definition_index_template", - "priority": 200, - "template": Object { - "aliases": Object { - "entities-service-history": Object {}, - }, - "mappings": Object { - "_meta": Object { - "version": "1.6.0", - }, - "date_detection": false, - "dynamic_templates": Array [ - Object { - "strings_as_keyword": Object { - "mapping": Object { - "fields": Object { - "text": Object { - "type": "text", - }, - }, - "ignore_above": 1024, - "type": "keyword", - }, - "match_mapping_type": "string", - }, - }, - Object { - "entity_metrics": Object { - "mapping": Object { - "type": "{dynamic_type}", - }, - "match_mapping_type": Array [ - "long", - "double", - ], - "path_match": "entity.metrics.*", - }, - }, - ], - }, - "settings": Object { - "index": Object { - "codec": "best_compression", - "mapping": Object { - "total_fields": Object { - "limit": 2000, - }, - }, - }, - }, - }, -} -`; - -exports[`generateEntitiesHistoryIndexTemplateConfig(definition) should generate a valid index template for custom definition 1`] = ` -Object { - "_meta": Object { - "description": "Index template for indices managed by the Elastic Entity Model's entity discovery framework for the history dataset", - "ecs_version": "8.0.0", - "managed": true, - "managed_by": "elastic_entity_model", - }, - "composed_of": Array [ - "entities_v1_history_base", - "entities_v1_entity", - "entities_v1_event", - "admin-console-services@platform", - "admin-console-services-history@platform", - "admin-console-services@custom", - "admin-console-services-history@custom", - ], - "ignore_missing_component_templates": Array [ - "admin-console-services@platform", - "admin-console-services-history@platform", - "admin-console-services@custom", - "admin-console-services-history@custom", - ], - "index_patterns": Array [ - ".entities.v1.history.admin-console-services.*", - ], - "name": "entities_v1_history_admin-console-services_index_template", - "priority": 200, - "template": Object { - "aliases": Object { - "entities-service-history": Object {}, - }, - "mappings": Object { - "_meta": Object { - "version": "1.6.0", - }, - "date_detection": false, - "dynamic_templates": Array [ - Object { - "strings_as_keyword": Object { - "mapping": Object { - "fields": Object { - "text": Object { - "type": "text", - }, - }, - "ignore_above": 1024, - "type": "keyword", - }, - "match_mapping_type": "string", - }, - }, - Object { - "entity_metrics": Object { - "mapping": Object { - "type": "{dynamic_type}", - }, - "match_mapping_type": Array [ - "long", - "double", - ], - "path_match": "entity.metrics.*", - }, - }, - ], - }, - "settings": Object { - "index": Object { - "codec": "best_compression", - "mapping": Object { - "total_fields": Object { - "limit": 2000, - }, - }, - }, - }, - }, -} -`; diff --git a/x-pack/plugins/entity_manager/server/lib/entities/templates/entities_history_template.test.ts b/x-pack/plugins/entity_manager/server/lib/entities/templates/entities_history_template.test.ts deleted file mode 100644 index 72e8d8591ab2d..0000000000000 --- a/x-pack/plugins/entity_manager/server/lib/entities/templates/entities_history_template.test.ts +++ /dev/null @@ -1,21 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { entityDefinition, builtInEntityDefinition } from '../helpers/fixtures'; -import { generateEntitiesHistoryIndexTemplateConfig } from './entities_history_template'; - -describe('generateEntitiesHistoryIndexTemplateConfig(definition)', () => { - it('should generate a valid index template for custom definition', () => { - const template = generateEntitiesHistoryIndexTemplateConfig(entityDefinition); - expect(template).toMatchSnapshot(); - }); - - it('should generate a valid index template for builtin definition', () => { - const template = generateEntitiesHistoryIndexTemplateConfig(builtInEntityDefinition); - expect(template).toMatchSnapshot(); - }); -}); diff --git a/x-pack/plugins/entity_manager/server/lib/entities/templates/entities_history_template.ts b/x-pack/plugins/entity_manager/server/lib/entities/templates/entities_history_template.ts deleted file mode 100644 index b1539d8108a6d..0000000000000 --- a/x-pack/plugins/entity_manager/server/lib/entities/templates/entities_history_template.ts +++ /dev/null @@ -1,96 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { IndicesPutIndexTemplateRequest } from '@elastic/elasticsearch/lib/api/types'; -import { - ENTITY_HISTORY, - EntityDefinition, - entitiesIndexPattern, - entitiesAliasPattern, - ENTITY_SCHEMA_VERSION_V1, -} from '@kbn/entities-schema'; -import { generateHistoryIndexTemplateId } from '../helpers/generate_component_id'; -import { - ENTITY_ENTITY_COMPONENT_TEMPLATE_V1, - ENTITY_EVENT_COMPONENT_TEMPLATE_V1, - ENTITY_HISTORY_BASE_COMPONENT_TEMPLATE_V1, -} from '../../../../common/constants_entities'; -import { getCustomHistoryTemplateComponents } from '../../../templates/components/helpers'; - -export const generateEntitiesHistoryIndexTemplateConfig = ( - definition: EntityDefinition -): IndicesPutIndexTemplateRequest => ({ - name: generateHistoryIndexTemplateId(definition), - _meta: { - description: - "Index template for indices managed by the Elastic Entity Model's entity discovery framework for the history dataset", - ecs_version: '8.0.0', - managed: true, - managed_by: 'elastic_entity_model', - }, - ignore_missing_component_templates: getCustomHistoryTemplateComponents(definition), - composed_of: [ - ENTITY_HISTORY_BASE_COMPONENT_TEMPLATE_V1, - ENTITY_ENTITY_COMPONENT_TEMPLATE_V1, - ENTITY_EVENT_COMPONENT_TEMPLATE_V1, - ...getCustomHistoryTemplateComponents(definition), - ], - index_patterns: [ - `${entitiesIndexPattern({ - schemaVersion: ENTITY_SCHEMA_VERSION_V1, - dataset: ENTITY_HISTORY, - definitionId: definition.id, - })}.*`, - ], - priority: 200, - template: { - aliases: { - [entitiesAliasPattern({ type: definition.type, dataset: ENTITY_HISTORY })]: {}, - }, - mappings: { - _meta: { - version: '1.6.0', - }, - date_detection: false, - dynamic_templates: [ - { - strings_as_keyword: { - mapping: { - ignore_above: 1024, - type: 'keyword', - fields: { - text: { - type: 'text', - }, - }, - }, - match_mapping_type: 'string', - }, - }, - { - entity_metrics: { - mapping: { - type: '{dynamic_type}', - }, - match_mapping_type: ['long', 'double'], - path_match: 'entity.metrics.*', - }, - }, - ], - }, - settings: { - index: { - codec: 'best_compression', - mapping: { - total_fields: { - limit: 2000, - }, - }, - }, - }, - }, -}); diff --git a/x-pack/plugins/entity_manager/server/lib/entities/templates/entities_latest_template.ts b/x-pack/plugins/entity_manager/server/lib/entities/templates/entities_latest_template.ts index ea476cf769644..e0c02c7471217 100644 --- a/x-pack/plugins/entity_manager/server/lib/entities/templates/entities_latest_template.ts +++ b/x-pack/plugins/entity_manager/server/lib/entities/templates/entities_latest_template.ts @@ -19,7 +19,7 @@ import { ENTITY_EVENT_COMPONENT_TEMPLATE_V1, ENTITY_LATEST_BASE_COMPONENT_TEMPLATE_V1, } from '../../../../common/constants_entities'; -import { getCustomLatestTemplateComponents } from '../../../templates/components/helpers'; +import { isBuiltinDefinition } from '../helpers/is_builtin_definition'; export const generateEntitiesLatestIndexTemplateConfig = ( definition: EntityDefinition @@ -94,3 +94,16 @@ export const generateEntitiesLatestIndexTemplateConfig = ( }, }, }); + +function getCustomLatestTemplateComponents(definition: EntityDefinition) { + if (isBuiltinDefinition(definition)) { + return []; + } + + return [ + `${definition.id}@platform`, // @platform goes before so it can be overwritten by custom + `${definition.id}-latest@platform`, + `${definition.id}@custom`, + `${definition.id}-latest@custom`, + ]; +} diff --git a/x-pack/plugins/entity_manager/server/lib/entities/transform/__snapshots__/generate_history_transform.test.ts.snap b/x-pack/plugins/entity_manager/server/lib/entities/transform/__snapshots__/generate_history_transform.test.ts.snap deleted file mode 100644 index b19a805b24b12..0000000000000 --- a/x-pack/plugins/entity_manager/server/lib/entities/transform/__snapshots__/generate_history_transform.test.ts.snap +++ /dev/null @@ -1,305 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`generateHistoryTransform(definition) should generate a valid history backfill transform 1`] = ` -Object { - "_meta": Object { - "definitionVersion": "999.999.999", - "managed": false, - }, - "defer_validation": true, - "dest": Object { - "index": ".entities.v1.history.noop", - "pipeline": "entities-v1-history-admin-console-services-backfill", - }, - "frequency": "5m", - "pivot": Object { - "aggs": Object { - "_errorRate_A": Object { - "filter": Object { - "bool": Object { - "minimum_should_match": 1, - "should": Array [ - Object { - "match_phrase": Object { - "log.level": "ERROR", - }, - }, - ], - }, - }, - }, - "_logRate_A": Object { - "filter": Object { - "bool": Object { - "minimum_should_match": 1, - "should": Array [ - Object { - "exists": Object { - "field": "log.level", - }, - }, - ], - }, - }, - }, - "entity.lastSeenTimestamp": Object { - "max": Object { - "field": "@timestamp", - }, - }, - "entity.metadata.host.name": Object { - "terms": Object { - "field": "host.name", - "size": 1000, - }, - }, - "entity.metadata.host.os.name": Object { - "terms": Object { - "field": "host.os.name", - "size": 1000, - }, - }, - "entity.metadata.sourceIndex": Object { - "terms": Object { - "field": "_index", - "size": 1000, - }, - }, - "entity.metadata.tags": Object { - "terms": Object { - "field": "tags", - "size": 1000, - }, - }, - "entity.metrics.errorRate": Object { - "bucket_script": Object { - "buckets_path": Object { - "A": "_errorRate_A>_count", - }, - "script": Object { - "lang": "painless", - "source": "params.A", - }, - }, - }, - "entity.metrics.logRate": Object { - "bucket_script": Object { - "buckets_path": Object { - "A": "_logRate_A>_count", - }, - "script": Object { - "lang": "painless", - "source": "params.A", - }, - }, - }, - }, - "group_by": Object { - "@timestamp": Object { - "date_histogram": Object { - "field": "@timestamp", - "fixed_interval": "1m", - }, - }, - "entity.identity.event.category": Object { - "terms": Object { - "field": "event.category", - "missing_bucket": true, - }, - }, - "entity.identity.log.logger": Object { - "terms": Object { - "field": "log.logger", - "missing_bucket": false, - }, - }, - }, - }, - "settings": Object { - "deduce_mappings": false, - "unattended": true, - }, - "source": Object { - "index": Array [ - "kbn-data-forge-fake_stack.*", - ], - "query": Object { - "bool": Object { - "filter": Array [ - Object { - "range": Object { - "@timestamp": Object { - "gte": "now-72h", - }, - }, - }, - Object { - "exists": Object { - "field": "log.logger", - }, - }, - ], - }, - }, - }, - "sync": Object { - "time": Object { - "delay": "15m", - "field": "@timestamp", - }, - }, - "transform_id": "entities-v1-history-backfill-admin-console-services-backfill", -} -`; - -exports[`generateHistoryTransform(definition) should generate a valid history transform 1`] = ` -Object { - "_meta": Object { - "definitionVersion": "1.0.0", - "managed": false, - }, - "defer_validation": true, - "dest": Object { - "index": ".entities.v1.history.noop", - "pipeline": "entities-v1-history-admin-console-services", - }, - "frequency": "2m", - "pivot": Object { - "aggs": Object { - "_errorRate_A": Object { - "filter": Object { - "bool": Object { - "minimum_should_match": 1, - "should": Array [ - Object { - "match_phrase": Object { - "log.level": "ERROR", - }, - }, - ], - }, - }, - }, - "_logRate_A": Object { - "filter": Object { - "bool": Object { - "minimum_should_match": 1, - "should": Array [ - Object { - "exists": Object { - "field": "log.level", - }, - }, - ], - }, - }, - }, - "entity.lastSeenTimestamp": Object { - "max": Object { - "field": "@timestamp", - }, - }, - "entity.metadata.host.name": Object { - "terms": Object { - "field": "host.name", - "size": 1000, - }, - }, - "entity.metadata.host.os.name": Object { - "terms": Object { - "field": "host.os.name", - "size": 1000, - }, - }, - "entity.metadata.sourceIndex": Object { - "terms": Object { - "field": "_index", - "size": 1000, - }, - }, - "entity.metadata.tags": Object { - "terms": Object { - "field": "tags", - "size": 1000, - }, - }, - "entity.metrics.errorRate": Object { - "bucket_script": Object { - "buckets_path": Object { - "A": "_errorRate_A>_count", - }, - "script": Object { - "lang": "painless", - "source": "params.A", - }, - }, - }, - "entity.metrics.logRate": Object { - "bucket_script": Object { - "buckets_path": Object { - "A": "_logRate_A>_count", - }, - "script": Object { - "lang": "painless", - "source": "params.A", - }, - }, - }, - }, - "group_by": Object { - "@timestamp": Object { - "date_histogram": Object { - "field": "@timestamp", - "fixed_interval": "1m", - }, - }, - "entity.identity.event.category": Object { - "terms": Object { - "field": "event.category", - "missing_bucket": true, - }, - }, - "entity.identity.log.logger": Object { - "terms": Object { - "field": "log.logger", - "missing_bucket": false, - }, - }, - }, - }, - "settings": Object { - "deduce_mappings": false, - "unattended": true, - }, - "source": Object { - "index": Array [ - "kbn-data-forge-fake_stack.*", - ], - "query": Object { - "bool": Object { - "filter": Array [ - Object { - "exists": Object { - "field": "log.logger", - }, - }, - Object { - "range": Object { - "@timestamp": Object { - "gte": "now-10m", - }, - }, - }, - ], - }, - }, - }, - "sync": Object { - "time": Object { - "delay": "2m", - "field": "@timestamp", - }, - }, - "transform_id": "entities-v1-history-admin-console-services", -} -`; diff --git a/x-pack/plugins/entity_manager/server/lib/entities/transform/__snapshots__/generate_latest_transform.test.ts.snap b/x-pack/plugins/entity_manager/server/lib/entities/transform/__snapshots__/generate_latest_transform.test.ts.snap index ab1224525f4d7..49f8ff4536120 100644 --- a/x-pack/plugins/entity_manager/server/lib/entities/transform/__snapshots__/generate_latest_transform.test.ts.snap +++ b/x-pack/plugins/entity_manager/server/lib/entities/transform/__snapshots__/generate_latest_transform.test.ts.snap @@ -14,76 +14,37 @@ Object { "frequency": "30s", "pivot": Object { "aggs": Object { - "_errorRate": Object { - "top_metrics": Object { - "metrics": Array [ - Object { - "field": "entity.metrics.errorRate", - }, - ], - "sort": Array [ - Object { - "@timestamp": "desc", - }, - ], - }, - }, - "_logRate": Object { - "top_metrics": Object { - "metrics": Array [ - Object { - "field": "entity.metrics.logRate", - }, - ], - "sort": Array [ - Object { - "@timestamp": "desc", - }, - ], - }, - }, - "entity.firstSeenTimestamp": Object { - "min": Object { - "field": "@timestamp", - }, - }, - "entity.identity.event.category": Object { - "aggs": Object { - "top_metric": Object { - "top_metrics": Object { - "metrics": Object { - "field": "event.category", - }, - "sort": "_score", - }, - }, - }, + "_errorRate_A": Object { "filter": Object { - "exists": Object { - "field": "event.category", - }, - }, - }, - "entity.identity.log.logger": Object { - "aggs": Object { - "top_metric": Object { - "top_metrics": Object { - "metrics": Object { - "field": "log.logger", + "bool": Object { + "minimum_should_match": 1, + "should": Array [ + Object { + "match_phrase": Object { + "log.level": "ERROR", + }, }, - "sort": "_score", - }, + ], }, }, + }, + "_logRate_A": Object { "filter": Object { - "exists": Object { - "field": "log.logger", + "bool": Object { + "minimum_should_match": 1, + "should": Array [ + Object { + "exists": Object { + "field": "log.level", + }, + }, + ], }, }, }, "entity.lastSeenTimestamp": Object { "max": Object { - "field": "entity.lastSeenTimestamp", + "field": "@timestamp", }, }, "entity.metadata.host.name": Object { @@ -91,14 +52,14 @@ Object { "data": Object { "terms": Object { "field": "host.name", - "size": 1000, + "size": 10, }, }, }, "filter": Object { "range": Object { "@timestamp": Object { - "gte": "now-360s", + "gte": "now-10m", }, }, }, @@ -108,14 +69,14 @@ Object { "data": Object { "terms": Object { "field": "host.os.name", - "size": 1000, + "size": 10, }, }, }, "filter": Object { "range": Object { "@timestamp": Object { - "gte": "now-360s", + "gte": "now-10m", }, }, }, @@ -124,15 +85,15 @@ Object { "aggs": Object { "data": Object { "terms": Object { - "field": "sourceIndex", - "size": 1000, + "field": "_index", + "size": 10, }, }, }, "filter": Object { "range": Object { "@timestamp": Object { - "gte": "now-360s", + "gte": "now-10m", }, }, }, @@ -142,14 +103,14 @@ Object { "data": Object { "terms": Object { "field": "tags", - "size": 1000, + "size": 10, }, }, }, "filter": Object { "range": Object { "@timestamp": Object { - "gte": "now-360s", + "gte": "now-10m", }, }, }, @@ -157,24 +118,37 @@ Object { "entity.metrics.errorRate": Object { "bucket_script": Object { "buckets_path": Object { - "value": "_errorRate[entity.metrics.errorRate]", + "A": "_errorRate_A>_count", + }, + "script": Object { + "lang": "painless", + "source": "params.A", }, - "script": "params.value", }, }, "entity.metrics.logRate": Object { "bucket_script": Object { "buckets_path": Object { - "value": "_logRate[entity.metrics.logRate]", + "A": "_logRate_A>_count", + }, + "script": Object { + "lang": "painless", + "source": "params.A", }, - "script": "params.value", }, }, }, "group_by": Object { - "entity.id": Object { + "entity.identity.event.category": Object { + "terms": Object { + "field": "event.category", + "missing_bucket": true, + }, + }, + "entity.identity.log.logger": Object { "terms": Object { - "field": "entity.id", + "field": "log.logger", + "missing_bucket": false, }, }, }, @@ -184,12 +158,32 @@ Object { "unattended": true, }, "source": Object { - "index": ".entities.v1.history.admin-console-services.*", + "index": Array [ + "kbn-data-forge-fake_stack.*", + ], + "query": Object { + "bool": Object { + "filter": Array [ + Object { + "exists": Object { + "field": "log.logger", + }, + }, + Object { + "range": Object { + "@timestamp": Object { + "gte": "now-10m", + }, + }, + }, + ], + }, + }, }, "sync": Object { "time": Object { - "delay": "1s", - "field": "event.ingested", + "delay": "10s", + "field": "@timestamp", }, }, "transform_id": "entities-v1-latest-admin-console-services", diff --git a/x-pack/plugins/entity_manager/server/lib/entities/transform/generate_history_transform.test.ts b/x-pack/plugins/entity_manager/server/lib/entities/transform/generate_history_transform.test.ts deleted file mode 100644 index f49ec0cd88a37..0000000000000 --- a/x-pack/plugins/entity_manager/server/lib/entities/transform/generate_history_transform.test.ts +++ /dev/null @@ -1,24 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { entityDefinition } from '../helpers/fixtures/entity_definition'; -import { entityDefinitionWithBackfill } from '../helpers/fixtures/entity_definition_with_backfill'; -import { - generateBackfillHistoryTransform, - generateHistoryTransform, -} from './generate_history_transform'; - -describe('generateHistoryTransform(definition)', () => { - it('should generate a valid history transform', () => { - const transform = generateHistoryTransform(entityDefinition); - expect(transform).toMatchSnapshot(); - }); - it('should generate a valid history backfill transform', () => { - const transform = generateBackfillHistoryTransform(entityDefinitionWithBackfill); - expect(transform).toMatchSnapshot(); - }); -}); diff --git a/x-pack/plugins/entity_manager/server/lib/entities/transform/generate_history_transform.ts b/x-pack/plugins/entity_manager/server/lib/entities/transform/generate_history_transform.ts deleted file mode 100644 index 239359738624c..0000000000000 --- a/x-pack/plugins/entity_manager/server/lib/entities/transform/generate_history_transform.ts +++ /dev/null @@ -1,178 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { EntityDefinition } from '@kbn/entities-schema'; -import { - QueryDslQueryContainer, - TransformPutTransformRequest, -} from '@elastic/elasticsearch/lib/api/types'; -import { getElasticsearchQueryOrThrow } from '../helpers/get_elasticsearch_query_or_throw'; -import { generateHistoryMetricAggregations } from './generate_metric_aggregations'; -import { - ENTITY_DEFAULT_HISTORY_FREQUENCY, - ENTITY_DEFAULT_HISTORY_SYNC_DELAY, -} from '../../../../common/constants_entities'; -import { generateHistoryMetadataAggregations } from './generate_metadata_aggregations'; -import { - generateHistoryTransformId, - generateHistoryIngestPipelineId, - generateHistoryIndexName, - generateHistoryBackfillTransformId, -} from '../helpers/generate_component_id'; -import { isBackfillEnabled } from '../helpers/is_backfill_enabled'; - -export function generateHistoryTransform( - definition: EntityDefinition -): TransformPutTransformRequest { - const filter: QueryDslQueryContainer[] = []; - - if (definition.filter) { - filter.push(getElasticsearchQueryOrThrow(definition.filter)); - } - - if (definition.identityFields.some(({ optional }) => !optional)) { - definition.identityFields - .filter(({ optional }) => !optional) - .forEach(({ field }) => { - filter.push({ exists: { field } }); - }); - } - - filter.push({ - range: { - [definition.history.timestampField]: { - gte: `now-${definition.history.settings.lookbackPeriod}`, - }, - }, - }); - - return generateTransformPutRequest({ - definition, - filter, - transformId: generateHistoryTransformId(definition), - frequency: definition.history.settings.frequency, - syncDelay: definition.history.settings.syncDelay, - }); -} - -export function generateBackfillHistoryTransform( - definition: EntityDefinition -): TransformPutTransformRequest { - if (!isBackfillEnabled(definition)) { - throw new Error( - 'generateBackfillHistoryTransform called without history.settings.backfillSyncDelay set' - ); - } - - const filter: QueryDslQueryContainer[] = []; - - if (definition.filter) { - filter.push(getElasticsearchQueryOrThrow(definition.filter)); - } - - if (definition.history.settings.backfillLookbackPeriod) { - filter.push({ - range: { - [definition.history.timestampField]: { - gte: `now-${definition.history.settings.backfillLookbackPeriod}`, - }, - }, - }); - } - - if (definition.identityFields.some(({ optional }) => !optional)) { - definition.identityFields - .filter(({ optional }) => !optional) - .forEach(({ field }) => { - filter.push({ exists: { field } }); - }); - } - - return generateTransformPutRequest({ - definition, - filter, - transformId: generateHistoryBackfillTransformId(definition), - frequency: definition.history.settings.backfillFrequency, - syncDelay: definition.history.settings.backfillSyncDelay, - }); -} - -const generateTransformPutRequest = ({ - definition, - filter, - transformId, - frequency, - syncDelay, -}: { - definition: EntityDefinition; - transformId: string; - filter: QueryDslQueryContainer[]; - frequency?: string; - syncDelay?: string; -}) => { - return { - transform_id: transformId, - _meta: { - definitionVersion: definition.version, - managed: definition.managed, - }, - defer_validation: true, - source: { - index: definition.indexPatterns, - ...(filter.length > 0 && { - query: { - bool: { - filter, - }, - }, - }), - }, - dest: { - index: `${generateHistoryIndexName({ id: 'noop' } as EntityDefinition)}`, - pipeline: generateHistoryIngestPipelineId(definition), - }, - frequency: frequency || ENTITY_DEFAULT_HISTORY_FREQUENCY, - sync: { - time: { - field: definition.history.settings.syncField || definition.history.timestampField, - delay: syncDelay || ENTITY_DEFAULT_HISTORY_SYNC_DELAY, - }, - }, - settings: { - deduce_mappings: false, - unattended: true, - }, - pivot: { - group_by: { - ...definition.identityFields.reduce( - (acc, id) => ({ - ...acc, - [`entity.identity.${id.field}`]: { - terms: { field: id.field, missing_bucket: id.optional }, - }, - }), - {} - ), - ['@timestamp']: { - date_histogram: { - field: definition.history.timestampField, - fixed_interval: definition.history.interval, - }, - }, - }, - aggs: { - ...generateHistoryMetricAggregations(definition), - ...generateHistoryMetadataAggregations(definition), - 'entity.lastSeenTimestamp': { - max: { - field: definition.history.timestampField, - }, - }, - }, - }, - }; -}; diff --git a/x-pack/plugins/entity_manager/server/lib/entities/transform/generate_latest_transform.ts b/x-pack/plugins/entity_manager/server/lib/entities/transform/generate_latest_transform.ts index 85ee57fefea2c..573bb2225f183 100644 --- a/x-pack/plugins/entity_manager/server/lib/entities/transform/generate_latest_transform.ts +++ b/x-pack/plugins/entity_manager/server/lib/entities/transform/generate_latest_transform.ts @@ -5,44 +5,97 @@ * 2.0. */ -import { TransformPutTransformRequest } from '@elastic/elasticsearch/lib/api/types'; import { EntityDefinition } from '@kbn/entities-schema'; +import { + QueryDslQueryContainer, + TransformPutTransformRequest, +} from '@elastic/elasticsearch/lib/api/types'; +import { getElasticsearchQueryOrThrow } from '../helpers/get_elasticsearch_query_or_throw'; +import { generateLatestMetricAggregations } from './generate_metric_aggregations'; import { ENTITY_DEFAULT_LATEST_FREQUENCY, ENTITY_DEFAULT_LATEST_SYNC_DELAY, } from '../../../../common/constants_entities'; import { - generateHistoryIndexName, - generateLatestIndexName, - generateLatestIngestPipelineId, generateLatestTransformId, + generateLatestIngestPipelineId, + generateLatestIndexName, } from '../helpers/generate_component_id'; -import { generateIdentityAggregations } from './generate_identity_aggregations'; import { generateLatestMetadataAggregations } from './generate_metadata_aggregations'; -import { generateLatestMetricAggregations } from './generate_metric_aggregations'; export function generateLatestTransform( definition: EntityDefinition ): TransformPutTransformRequest { + const filter: QueryDslQueryContainer[] = []; + + if (definition.filter) { + filter.push(getElasticsearchQueryOrThrow(definition.filter)); + } + + if (definition.identityFields.some(({ optional }) => !optional)) { + definition.identityFields + .filter(({ optional }) => !optional) + .forEach(({ field }) => { + filter.push({ exists: { field } }); + }); + } + + filter.push({ + range: { + [definition.latest.timestampField]: { + gte: `now-${definition.latest.lookbackPeriod}`, + }, + }, + }); + + return generateTransformPutRequest({ + definition, + filter, + transformId: generateLatestTransformId(definition), + frequency: definition.latest.settings?.frequency ?? ENTITY_DEFAULT_LATEST_FREQUENCY, + syncDelay: definition.latest.settings?.syncDelay ?? ENTITY_DEFAULT_LATEST_SYNC_DELAY, + }); +} + +const generateTransformPutRequest = ({ + definition, + filter, + transformId, + frequency, + syncDelay, +}: { + definition: EntityDefinition; + transformId: string; + filter: QueryDslQueryContainer[]; + frequency: string; + syncDelay: string; +}) => { return { - transform_id: generateLatestTransformId(definition), + transform_id: transformId, _meta: { definitionVersion: definition.version, managed: definition.managed, }, defer_validation: true, source: { - index: `${generateHistoryIndexName(definition)}.*`, + index: definition.indexPatterns, + ...(filter.length > 0 && { + query: { + bool: { + filter, + }, + }, + }), }, dest: { index: `${generateLatestIndexName({ id: 'noop' } as EntityDefinition)}`, pipeline: generateLatestIngestPipelineId(definition), }, - frequency: definition.latest?.settings?.frequency ?? ENTITY_DEFAULT_LATEST_FREQUENCY, + frequency, sync: { time: { - field: definition.latest?.settings?.syncField ?? 'event.ingested', - delay: definition.latest?.settings?.syncDelay ?? ENTITY_DEFAULT_LATEST_SYNC_DELAY, + field: definition.latest.settings?.syncField || definition.latest.timestampField, + delay: syncDelay, }, }, settings: { @@ -51,25 +104,25 @@ export function generateLatestTransform( }, pivot: { group_by: { - ['entity.id']: { - terms: { field: 'entity.id' }, - }, + ...definition.identityFields.reduce( + (acc, id) => ({ + ...acc, + [`entity.identity.${id.field}`]: { + terms: { field: id.field, missing_bucket: id.optional }, + }, + }), + {} + ), }, aggs: { ...generateLatestMetricAggregations(definition), ...generateLatestMetadataAggregations(definition), - ...generateIdentityAggregations(definition), 'entity.lastSeenTimestamp': { max: { - field: 'entity.lastSeenTimestamp', - }, - }, - 'entity.firstSeenTimestamp': { - min: { - field: '@timestamp', + field: definition.latest.timestampField, }, }, }, }, }; -} +}; diff --git a/x-pack/plugins/entity_manager/server/lib/entities/transform/generate_metadata_aggregations.test.ts b/x-pack/plugins/entity_manager/server/lib/entities/transform/generate_metadata_aggregations.test.ts index 7746be66f5033..12535d313143b 100644 --- a/x-pack/plugins/entity_manager/server/lib/entities/transform/generate_metadata_aggregations.test.ts +++ b/x-pack/plugins/entity_manager/server/lib/entities/transform/generate_metadata_aggregations.test.ts @@ -7,134 +7,22 @@ import { entityDefinitionSchema } from '@kbn/entities-schema'; import { rawEntityDefinition } from '../helpers/fixtures/entity_definition'; -import { - generateHistoryMetadataAggregations, - generateLatestMetadataAggregations, -} from './generate_metadata_aggregations'; +import { generateLatestMetadataAggregations } from './generate_metadata_aggregations'; describe('Generate Metadata Aggregations for history and latest', () => { - describe('generateHistoryMetadataAggregations()', () => { - it('should generate metadata aggregations for string format', () => { - const definition = entityDefinitionSchema.parse({ - ...rawEntityDefinition, - metadata: ['host.name'], - }); - expect(generateHistoryMetadataAggregations(definition)).toEqual({ - 'entity.metadata.host.name': { - terms: { - field: 'host.name', - size: 1000, - }, - }, - }); - }); - - it('should generate metadata aggregations for object format with only source', () => { - const definition = entityDefinitionSchema.parse({ - ...rawEntityDefinition, - metadata: [{ source: 'host.name' }], - }); - expect(generateHistoryMetadataAggregations(definition)).toEqual({ - 'entity.metadata.host.name': { - terms: { - field: 'host.name', - size: 1000, - }, - }, - }); - }); - - it('should generate metadata aggregations for object format with source and aggregation', () => { - const definition = entityDefinitionSchema.parse({ - ...rawEntityDefinition, - metadata: [{ source: 'host.name', aggregation: { type: 'terms', limit: 10 } }], - }); - expect(generateHistoryMetadataAggregations(definition)).toEqual({ - 'entity.metadata.host.name': { - terms: { - field: 'host.name', - size: 10, - }, - }, - }); - }); - - it('should generate metadata aggregations for object format with source, aggregation, and destination', () => { - const definition = entityDefinitionSchema.parse({ - ...rawEntityDefinition, - metadata: [ - { - source: 'host.name', - aggregation: { type: 'terms', limit: 20 }, - destination: 'hostName', - }, - ], - }); - expect(generateHistoryMetadataAggregations(definition)).toEqual({ - 'entity.metadata.hostName': { - terms: { - field: 'host.name', - size: 20, - }, - }, - }); - }); - - it('should generate metadata aggregations for terms and top_value', () => { - const definition = entityDefinitionSchema.parse({ - ...rawEntityDefinition, - metadata: [ - { - source: 'host.name', - aggregation: { type: 'terms', limit: 10 }, - destination: 'hostName', - }, - { - source: 'agent.name', - aggregation: { type: 'top_value', sort: { '@timestamp': 'desc' } }, - destination: 'agentName', - }, - ], - }); - - expect(generateHistoryMetadataAggregations(definition)).toEqual({ - 'entity.metadata.hostName': { - terms: { - field: 'host.name', - size: 10, - }, - }, - 'entity.metadata.agentName': { - filter: { - exists: { - field: 'agent.name', - }, - }, - aggs: { - top_value: { - top_metrics: { - metrics: { field: 'agent.name' }, - sort: { '@timestamp': 'desc' }, - }, - }, - }, - }, - }); - }); - }); - describe('generateLatestMetadataAggregations()', () => { it('should generate metadata aggregations for string format', () => { const definition = entityDefinitionSchema.parse({ ...rawEntityDefinition, metadata: ['host.name'], }); + expect(generateLatestMetadataAggregations(definition)).toEqual({ 'entity.metadata.host.name': { filter: { range: { '@timestamp': { - gte: 'now-360s', + gte: 'now-10m', }, }, }, @@ -142,7 +30,7 @@ describe('Generate Metadata Aggregations for history and latest', () => { data: { terms: { field: 'host.name', - size: 1000, + size: 10, }, }, }, @@ -160,7 +48,7 @@ describe('Generate Metadata Aggregations for history and latest', () => { filter: { range: { '@timestamp': { - gte: 'now-360s', + gte: 'now-10m', }, }, }, @@ -168,7 +56,7 @@ describe('Generate Metadata Aggregations for history and latest', () => { data: { terms: { field: 'host.name', - size: 1000, + size: 10, }, }, }, @@ -179,14 +67,16 @@ describe('Generate Metadata Aggregations for history and latest', () => { it('should generate metadata aggregations for object format with source and aggregation', () => { const definition = entityDefinitionSchema.parse({ ...rawEntityDefinition, - metadata: [{ source: 'host.name', aggregation: { type: 'terms', limit: 10 } }], + metadata: [ + { source: 'host.name', aggregation: { type: 'terms', limit: 10, lookbackPeriod: '1h' } }, + ], }); expect(generateLatestMetadataAggregations(definition)).toEqual({ 'entity.metadata.host.name': { filter: { range: { '@timestamp': { - gte: 'now-360s', + gte: 'now-1h', }, }, }, @@ -218,14 +108,14 @@ describe('Generate Metadata Aggregations for history and latest', () => { filter: { range: { '@timestamp': { - gte: 'now-360s', + gte: 'now-10m', }, }, }, aggs: { data: { terms: { - field: 'hostName', + field: 'host.name', size: 10, }, }, @@ -255,14 +145,14 @@ describe('Generate Metadata Aggregations for history and latest', () => { filter: { range: { '@timestamp': { - gte: 'now-360s', + gte: 'now-10m', }, }, }, aggs: { data: { terms: { - field: 'hostName', + field: 'host.name', size: 10, }, }, @@ -275,13 +165,13 @@ describe('Generate Metadata Aggregations for history and latest', () => { { range: { '@timestamp': { - gte: 'now-360s', + gte: 'now-10m', }, }, }, { exists: { - field: 'agentName', + field: 'agent.name', }, }, ], @@ -291,7 +181,7 @@ describe('Generate Metadata Aggregations for history and latest', () => { top_value: { top_metrics: { metrics: { - field: 'agentName', + field: 'agent.name', }, sort: { '@timestamp': 'desc', diff --git a/x-pack/plugins/entity_manager/server/lib/entities/transform/generate_metadata_aggregations.ts b/x-pack/plugins/entity_manager/server/lib/entities/transform/generate_metadata_aggregations.ts index 0fc4464672219..796d1e25b55ec 100644 --- a/x-pack/plugins/entity_manager/server/lib/entities/transform/generate_metadata_aggregations.ts +++ b/x-pack/plugins/entity_manager/server/lib/entities/transform/generate_metadata_aggregations.ts @@ -6,70 +6,28 @@ */ import { EntityDefinition } from '@kbn/entities-schema'; -import { calculateOffset } from '../helpers/calculate_offset'; - -export function generateHistoryMetadataAggregations(definition: EntityDefinition) { - if (!definition.metadata) { - return {}; - } - return definition.metadata.reduce((aggs, metadata) => { - let agg; - if (metadata.aggregation.type === 'terms') { - agg = { - terms: { - field: metadata.source, - size: metadata.aggregation.limit, - }, - }; - } else if (metadata.aggregation.type === 'top_value') { - agg = { - filter: { - exists: { - field: metadata.source, - }, - }, - aggs: { - top_value: { - top_metrics: { - metrics: { - field: metadata.source, - }, - sort: metadata.aggregation.sort, - }, - }, - }, - }; - } - - return { - ...aggs, - [`entity.metadata.${metadata.destination}`]: agg, - }; - }, {}); -} export function generateLatestMetadataAggregations(definition: EntityDefinition) { if (!definition.metadata) { return {}; } - const offsetInSeconds = `${calculateOffset(definition)}s`; - return definition.metadata.reduce((aggs, metadata) => { + const lookbackPeriod = metadata.aggregation.lookbackPeriod || definition.latest.lookbackPeriod; let agg; if (metadata.aggregation.type === 'terms') { agg = { filter: { range: { '@timestamp': { - gte: `now-${offsetInSeconds}`, + gte: `now-${lookbackPeriod}`, }, }, }, aggs: { data: { terms: { - field: metadata.destination, + field: metadata.source, size: metadata.aggregation.limit, }, }, @@ -83,13 +41,13 @@ export function generateLatestMetadataAggregations(definition: EntityDefinition) { range: { '@timestamp': { - gte: `now-${metadata.aggregation.lookbackPeriod ?? offsetInSeconds}`, + gte: `now-${lookbackPeriod}`, }, }, }, { exists: { - field: metadata.destination, + field: metadata.source, }, }, ], @@ -99,7 +57,7 @@ export function generateLatestMetadataAggregations(definition: EntityDefinition) top_value: { top_metrics: { metrics: { - field: metadata.destination, + field: metadata.source, }, sort: metadata.aggregation.sort, }, diff --git a/x-pack/plugins/entity_manager/server/lib/entities/transform/generate_metric_aggregations.ts b/x-pack/plugins/entity_manager/server/lib/entities/transform/generate_metric_aggregations.ts index bd1af365116cb..d42dd69b37eff 100644 --- a/x-pack/plugins/entity_manager/server/lib/entities/transform/generate_metric_aggregations.ts +++ b/x-pack/plugins/entity_manager/server/lib/entities/transform/generate_metric_aggregations.ts @@ -104,41 +104,15 @@ function buildMetricEquation(keyMetric: KeyMetric) { }; } -export function generateHistoryMetricAggregations(definition: EntityDefinition) { - if (!definition.metrics) { - return {}; - } - return definition.metrics.reduce((aggs, keyMetric) => { - return { - ...aggs, - ...buildMetricAggregations(keyMetric, definition.history.timestampField), - [`entity.metrics.${keyMetric.name}`]: buildMetricEquation(keyMetric), - }; - }, {}); -} - export function generateLatestMetricAggregations(definition: EntityDefinition) { if (!definition.metrics) { return {}; } - return definition.metrics.reduce((aggs, keyMetric) => { return { ...aggs, - [`_${keyMetric.name}`]: { - top_metrics: { - metrics: [{ field: `entity.metrics.${keyMetric.name}` }], - sort: [{ '@timestamp': 'desc' }], - }, - }, - [`entity.metrics.${keyMetric.name}`]: { - bucket_script: { - buckets_path: { - value: `_${keyMetric.name}[entity.metrics.${keyMetric.name}]`, - }, - script: 'params.value', - }, - }, + ...buildMetricAggregations(keyMetric, definition.latest.timestampField), + [`entity.metrics.${keyMetric.name}`]: buildMetricEquation(keyMetric), }; }, {}); } diff --git a/x-pack/plugins/entity_manager/server/lib/entities/transform/validate_transform_ids.ts b/x-pack/plugins/entity_manager/server/lib/entities/transform/validate_transform_ids.ts index c16b7f126dded..c703124bdf082 100644 --- a/x-pack/plugins/entity_manager/server/lib/entities/transform/validate_transform_ids.ts +++ b/x-pack/plugins/entity_manager/server/lib/entities/transform/validate_transform_ids.ts @@ -7,26 +7,14 @@ import { EntityDefinition } from '@kbn/entities-schema'; import { EntityDefinitionIdInvalid } from '../errors/entity_definition_id_invalid'; -import { - generateHistoryBackfillTransformId, - generateHistoryTransformId, - generateLatestTransformId, -} from '../helpers/generate_component_id'; +import { generateLatestTransformId } from '../helpers/generate_component_id'; const TRANSFORM_ID_MAX_LENGTH = 64; export function validateDefinitionCanCreateValidTransformIds(definition: EntityDefinition) { - const historyTransformId = generateHistoryTransformId(definition); const latestTransformId = generateLatestTransformId(definition); - const historyBackfillTransformId = generateHistoryBackfillTransformId(definition); - const spareChars = - TRANSFORM_ID_MAX_LENGTH - - Math.max( - historyTransformId.length, - latestTransformId.length, - historyBackfillTransformId.length - ); + const spareChars = TRANSFORM_ID_MAX_LENGTH - latestTransformId.length; if (spareChars < 0) { throw new EntityDefinitionIdInvalid( diff --git a/x-pack/plugins/entity_manager/server/lib/entities/uninstall_entity_definition.ts b/x-pack/plugins/entity_manager/server/lib/entities/uninstall_entity_definition.ts index 8bc8efa3870aa..d0e0410b6e422 100644 --- a/x-pack/plugins/entity_manager/server/lib/entities/uninstall_entity_definition.ts +++ b/x-pack/plugins/entity_manager/server/lib/entities/uninstall_entity_definition.ts @@ -11,14 +11,10 @@ import { EntityDefinition } from '@kbn/entities-schema'; import { Logger } from '@kbn/logging'; import { deleteEntityDefinition } from './delete_entity_definition'; import { deleteIndices } from './delete_index'; -import { deleteHistoryIngestPipeline, deleteLatestIngestPipeline } from './delete_ingest_pipeline'; +import { deleteIngestPipelines } from './delete_ingest_pipeline'; import { findEntityDefinitions } from './find_entity_definition'; -import { - generateHistoryIndexTemplateId, - generateLatestIndexTemplateId, -} from './helpers/generate_component_id'; -import { deleteTemplate } from '../manage_index_templates'; +import { deleteTemplates } from '../manage_index_templates'; import { stopTransforms } from './stop_transforms'; @@ -40,19 +36,13 @@ export async function uninstallEntityDefinition({ await stopTransforms(esClient, definition, logger); await deleteTransforms(esClient, definition, logger); - await Promise.all([ - deleteHistoryIngestPipeline(esClient, definition, logger), - deleteLatestIngestPipeline(esClient, definition, logger), - ]); + await deleteIngestPipelines(esClient, definition, logger); if (deleteData) { await deleteIndices(esClient, definition, logger); } - await Promise.all([ - deleteTemplate({ esClient, logger, name: generateHistoryIndexTemplateId(definition) }), - deleteTemplate({ esClient, logger, name: generateLatestIndexTemplateId(definition) }), - ]); + await deleteTemplates(esClient, definition, logger); await deleteEntityDefinition(soClient, definition); } diff --git a/x-pack/plugins/entity_manager/server/lib/entity_client.ts b/x-pack/plugins/entity_manager/server/lib/entity_client.ts index ee6b59b0ae0ea..710872c04eda0 100644 --- a/x-pack/plugins/entity_manager/server/lib/entity_client.ts +++ b/x-pack/plugins/entity_manager/server/lib/entity_client.ts @@ -41,7 +41,7 @@ export class EntityClient { }); if (!installOnly) { - await startTransforms(this.options.esClient, definition, this.options.logger); + await startTransforms(this.options.esClient, installedDefinition, this.options.logger); } return installedDefinition; diff --git a/x-pack/plugins/entity_manager/server/lib/manage_index_templates.ts b/x-pack/plugins/entity_manager/server/lib/manage_index_templates.ts index b0789b6cf2769..ffa58cd9c0145 100644 --- a/x-pack/plugins/entity_manager/server/lib/manage_index_templates.ts +++ b/x-pack/plugins/entity_manager/server/lib/manage_index_templates.ts @@ -5,6 +5,7 @@ * 2.0. */ +import { EntityDefinition } from '@kbn/entities-schema'; import { ClusterPutComponentTemplateRequest, IndicesPutIndexTemplateRequest, @@ -15,6 +16,7 @@ import { entitiesLatestBaseComponentTemplateConfig } from '../templates/componen import { entitiesEntityComponentTemplateConfig } from '../templates/components/entity'; import { entitiesEventComponentTemplateConfig } from '../templates/components/event'; import { retryTransientEsErrors } from './entities/helpers/retry'; +import { generateEntitiesLatestIndexTemplateConfig } from './entities/templates/entities_latest_template'; interface TemplateManagementOptions { esClient: ElasticsearchClient; @@ -67,14 +69,27 @@ interface DeleteTemplateOptions { export async function upsertTemplate({ esClient, template, logger }: TemplateManagementOptions) { try { - await retryTransientEsErrors(() => esClient.indices.putIndexTemplate(template), { logger }); + const result = await retryTransientEsErrors(() => esClient.indices.putIndexTemplate(template), { + logger, + }); logger.debug(() => `Installed entity manager index template: ${JSON.stringify(template)}`); + return result; } catch (error: any) { logger.error(`Error updating entity manager index template: ${error.message}`); throw error; } } +export async function createAndInstallTemplates( + esClient: ElasticsearchClient, + definition: EntityDefinition, + logger: Logger +): Promise> { + const template = generateEntitiesLatestIndexTemplateConfig(definition); + await upsertTemplate({ esClient, template, logger }); + return [{ type: 'template', id: template.name }]; +} + export async function deleteTemplate({ esClient, name, logger }: DeleteTemplateOptions) { try { await retryTransientEsErrors( @@ -87,6 +102,28 @@ export async function deleteTemplate({ esClient, name, logger }: DeleteTemplateO } } +export async function deleteTemplates( + esClient: ElasticsearchClient, + definition: EntityDefinition, + logger: Logger +) { + try { + await Promise.all( + (definition.installedComponents ?? []) + .filter(({ type }) => type === 'template') + .map(({ id }) => + retryTransientEsErrors( + () => esClient.indices.deleteIndexTemplate({ name: id }, { ignore: [404] }), + { logger } + ) + ) + ); + } catch (error: any) { + logger.error(`Error deleting entity manager index template: ${error.message}`); + throw error; + } +} + export async function upsertComponent({ esClient, component, logger }: ComponentManagementOptions) { try { await retryTransientEsErrors(() => esClient.cluster.putComponentTemplate(component), { diff --git a/x-pack/plugins/entity_manager/server/routes/enablement/disable.ts b/x-pack/plugins/entity_manager/server/routes/enablement/disable.ts index bde68eb85ba9f..9c1c4f403636b 100644 --- a/x-pack/plugins/entity_manager/server/routes/enablement/disable.ts +++ b/x-pack/plugins/entity_manager/server/routes/enablement/disable.ts @@ -51,8 +51,8 @@ export const disableEntityDiscoveryRoute = createEntityManagerServerRoute({ }), handler: async ({ context, response, params, logger, server }) => { try { - const esClient = (await context.core).elasticsearch.client.asCurrentUser; - const canDisable = await canDisableEntityDiscovery(esClient); + const esClientAsCurrentUser = (await context.core).elasticsearch.client.asCurrentUser; + const canDisable = await canDisableEntityDiscovery(esClientAsCurrentUser); if (!canDisable) { return response.forbidden({ body: { @@ -62,6 +62,7 @@ export const disableEntityDiscoveryRoute = createEntityManagerServerRoute({ }); } + const esClient = (await context.core).elasticsearch.client.asSecondaryAuthUser; const soClient = (await context.core).savedObjects.getClient({ includedHiddenTypes: [EntityDiscoveryApiKeyType.name], }); diff --git a/x-pack/plugins/entity_manager/server/routes/enablement/enable.ts b/x-pack/plugins/entity_manager/server/routes/enablement/enable.ts index 9814840d20a0b..1002c1e716df2 100644 --- a/x-pack/plugins/entity_manager/server/routes/enablement/enable.ts +++ b/x-pack/plugins/entity_manager/server/routes/enablement/enable.ts @@ -80,8 +80,10 @@ export const enableEntityDiscoveryRoute = createEntityManagerServerRoute({ }); } - const esClient = (await context.core).elasticsearch.client.asCurrentUser; - const canEnable = await canEnableEntityDiscovery(esClient); + const core = await context.core; + + const esClientAsCurrentUser = core.elasticsearch.client.asCurrentUser; + const canEnable = await canEnableEntityDiscovery(esClientAsCurrentUser); if (!canEnable) { return response.forbidden({ body: { @@ -91,7 +93,7 @@ export const enableEntityDiscoveryRoute = createEntityManagerServerRoute({ }); } - const soClient = (await context.core).savedObjects.getClient({ + const soClient = core.savedObjects.getClient({ includedHiddenTypes: [EntityDiscoveryApiKeyType.name], }); const existingApiKey = await readEntityDiscoveryAPIKey(server); @@ -117,6 +119,7 @@ export const enableEntityDiscoveryRoute = createEntityManagerServerRoute({ await saveEntityDiscoveryAPIKey(soClient, apiKey); + const esClient = core.elasticsearch.client.asSecondaryAuthUser; const installedDefinitions = await installBuiltInEntityDefinitions({ esClient, soClient, diff --git a/x-pack/plugins/entity_manager/server/routes/entities/reset.ts b/x-pack/plugins/entity_manager/server/routes/entities/reset.ts index a59c38b3acf7c..0b6942e335e51 100644 --- a/x-pack/plugins/entity_manager/server/routes/entities/reset.ts +++ b/x-pack/plugins/entity_manager/server/routes/entities/reset.ts @@ -12,25 +12,13 @@ import { EntitySecurityException } from '../../lib/entities/errors/entity_securi import { InvalidTransformError } from '../../lib/entities/errors/invalid_transform_error'; import { readEntityDefinition } from '../../lib/entities/read_entity_definition'; -import { - deleteHistoryIngestPipeline, - deleteLatestIngestPipeline, -} from '../../lib/entities/delete_ingest_pipeline'; +import { deleteIngestPipelines } from '../../lib/entities/delete_ingest_pipeline'; import { deleteIndices } from '../../lib/entities/delete_index'; -import { - createAndInstallHistoryIngestPipeline, - createAndInstallLatestIngestPipeline, -} from '../../lib/entities/create_and_install_ingest_pipeline'; -import { - createAndInstallHistoryBackfillTransform, - createAndInstallHistoryTransform, - createAndInstallLatestTransform, -} from '../../lib/entities/create_and_install_transform'; +import { createAndInstallIngestPipelines } from '../../lib/entities/create_and_install_ingest_pipeline'; +import { createAndInstallTransforms } from '../../lib/entities/create_and_install_transform'; import { startTransforms } from '../../lib/entities/start_transforms'; import { EntityDefinitionNotFound } from '../../lib/entities/errors/entity_not_found'; -import { isBackfillEnabled } from '../../lib/entities/helpers/is_backfill_enabled'; - import { createEntityManagerServerRoute } from '../create_entity_manager_server_route'; import { deleteTransforms } from '../../lib/entities/delete_transforms'; import { stopTransforms } from '../../lib/entities/stop_transforms'; @@ -51,18 +39,12 @@ export const resetEntityDefinitionRoute = createEntityManagerServerRoute({ await stopTransforms(esClient, definition, logger); await deleteTransforms(esClient, definition, logger); - await deleteHistoryIngestPipeline(esClient, definition, logger); - await deleteLatestIngestPipeline(esClient, definition, logger); + await deleteIngestPipelines(esClient, definition, logger); await deleteIndices(esClient, definition, logger); // Recreate everything - await createAndInstallHistoryIngestPipeline(esClient, definition, logger); - await createAndInstallLatestIngestPipeline(esClient, definition, logger); - await createAndInstallHistoryTransform(esClient, definition, logger); - if (isBackfillEnabled(definition)) { - await createAndInstallHistoryBackfillTransform(esClient, definition, logger); - } - await createAndInstallLatestTransform(esClient, definition, logger); + await createAndInstallIngestPipelines(esClient, definition, logger); + await createAndInstallTransforms(esClient, definition, logger); await startTransforms(esClient, definition, logger); return response.ok({ body: { acknowledged: true } }); diff --git a/x-pack/plugins/entity_manager/server/saved_objects/entity_definition.ts b/x-pack/plugins/entity_manager/server/saved_objects/entity_definition.ts index fdf2510e8627e..bdea2b71e4141 100644 --- a/x-pack/plugins/entity_manager/server/saved_objects/entity_definition.ts +++ b/x-pack/plugins/entity_manager/server/saved_objects/entity_definition.ts @@ -5,11 +5,36 @@ * 2.0. */ +import { SavedObjectModelDataBackfillFn } from '@kbn/core-saved-objects-server'; import { SavedObject, SavedObjectsType } from '@kbn/core/server'; import { EntityDefinition } from '@kbn/entities-schema'; +import { + generateHistoryIndexTemplateId, + generateHistoryIngestPipelineId, + generateHistoryTransformId, + generateLatestIndexTemplateId, + generateLatestIngestPipelineId, + generateLatestTransformId, +} from '../lib/entities/helpers/generate_component_id'; export const SO_ENTITY_DEFINITION_TYPE = 'entity-definition'; +export const backfillInstalledComponents: SavedObjectModelDataBackfillFn< + EntityDefinition, + EntityDefinition +> = (savedObject) => { + const definition = savedObject.attributes; + definition.installedComponents = [ + { type: 'transform', id: generateHistoryTransformId(definition) }, + { type: 'transform', id: generateLatestTransformId(definition) }, + { type: 'ingest_pipeline', id: generateHistoryIngestPipelineId(definition) }, + { type: 'ingest_pipeline', id: generateLatestIngestPipelineId(definition) }, + { type: 'template', id: generateHistoryIndexTemplateId(definition) }, + { type: 'template', id: generateLatestIndexTemplateId(definition) }, + ]; + return savedObject; +}; + export const entityDefinition: SavedObjectsType = { name: SO_ENTITY_DEFINITION_TYPE, hidden: false, @@ -64,5 +89,13 @@ export const entityDefinition: SavedObjectsType = { }, ], }, + '3': { + changes: [ + { + type: 'data_backfill', + backfillFn: backfillInstalledComponents, + }, + ], + }, }, }; diff --git a/x-pack/plugins/entity_manager/server/templates/components/helpers.test.ts b/x-pack/plugins/entity_manager/server/templates/components/helpers.test.ts deleted file mode 100644 index 90c5e90d43f3a..0000000000000 --- a/x-pack/plugins/entity_manager/server/templates/components/helpers.test.ts +++ /dev/null @@ -1,31 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { EntityDefinition } from '@kbn/entities-schema'; -import { getCustomHistoryTemplateComponents, getCustomLatestTemplateComponents } from './helpers'; - -describe('helpers', () => { - it('getCustomLatestTemplateComponents should return template component in the right sort order', () => { - const result = getCustomLatestTemplateComponents({ id: 'test' } as EntityDefinition); - expect(result).toEqual([ - 'test@platform', - 'test-latest@platform', - 'test@custom', - 'test-latest@custom', - ]); - }); - - it('getCustomHistoryTemplateComponents should return template component in the right sort order', () => { - const result = getCustomHistoryTemplateComponents({ id: 'test' } as EntityDefinition); - expect(result).toEqual([ - 'test@platform', - 'test-history@platform', - 'test@custom', - 'test-history@custom', - ]); - }); -}); diff --git a/x-pack/plugins/entity_manager/server/templates/components/helpers.ts b/x-pack/plugins/entity_manager/server/templates/components/helpers.ts deleted file mode 100644 index 23cc7cccb6a13..0000000000000 --- a/x-pack/plugins/entity_manager/server/templates/components/helpers.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 { EntityDefinition } from '@kbn/entities-schema'; -import { isBuiltinDefinition } from '../../lib/entities/helpers/is_builtin_definition'; - -export const getCustomLatestTemplateComponents = (definition: EntityDefinition) => { - if (isBuiltinDefinition(definition)) { - return []; - } - - return [ - `${definition.id}@platform`, // @platform goes before so it can be overwritten by custom - `${definition.id}-latest@platform`, - `${definition.id}@custom`, - `${definition.id}-latest@custom`, - ]; -}; - -export const getCustomHistoryTemplateComponents = (definition: EntityDefinition) => { - if (isBuiltinDefinition(definition)) { - return []; - } - - return [ - `${definition.id}@platform`, // @platform goes before so it can be overwritten by custom - `${definition.id}-history@platform`, - `${definition.id}@custom`, - `${definition.id}-history@custom`, - ]; -}; diff --git a/x-pack/plugins/entity_manager/tsconfig.json b/x-pack/plugins/entity_manager/tsconfig.json index 29c100ee4c9d2..34c57a27dd829 100644 --- a/x-pack/plugins/entity_manager/tsconfig.json +++ b/x-pack/plugins/entity_manager/tsconfig.json @@ -34,5 +34,6 @@ "@kbn/zod-helpers", "@kbn/encrypted-saved-objects-plugin", "@kbn/licensing-plugin", + "@kbn/core-saved-objects-server", ] } diff --git a/x-pack/plugins/fleet/common/types/models/package_spec.ts b/x-pack/plugins/fleet/common/types/models/package_spec.ts index 24a592490137c..18c10e4617417 100644 --- a/x-pack/plugins/fleet/common/types/models/package_spec.ts +++ b/x-pack/plugins/fleet/common/types/models/package_spec.ts @@ -18,7 +18,7 @@ export interface PackageSpecManifest { source?: { license: string; }; - type?: 'integration' | 'input'; + type?: PackageSpecPackageType; release?: 'experimental' | 'beta' | 'ga'; categories?: Array; conditions?: PackageSpecConditions; @@ -35,6 +35,11 @@ export interface PackageSpecManifest { privileges?: { root?: boolean }; }; asset_tags?: PackageSpecTags[]; + discovery?: { + fields?: Array<{ + name: string; + }>; + }; } export interface PackageSpecTags { text: string; @@ -42,7 +47,7 @@ export interface PackageSpecTags { asset_ids?: string[]; } -export type PackageSpecPackageType = 'integration' | 'input'; +export type PackageSpecPackageType = 'integration' | 'input' | 'content'; export type PackageSpecCategory = | 'advanced_analytics_ueba' diff --git a/x-pack/plugins/fleet/cypress.config.space_awareness.ts b/x-pack/plugins/fleet/cypress.config.space_awareness.ts new file mode 100644 index 0000000000000..6efda828e6fbc --- /dev/null +++ b/x-pack/plugins/fleet/cypress.config.space_awareness.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 { defineCypressConfig } from '@kbn/cypress-config'; + +// eslint-disable-next-line import/no-default-export +export default defineCypressConfig({ + defaultCommandTimeout: 60000, + requestTimeout: 60000, + responseTimeout: 60000, + execTimeout: 120000, + pageLoadTimeout: 120000, + + retries: { + runMode: 2, + }, + + env: { + grepFilterSpecs: false, + }, + + screenshotsFolder: '../../../target/kibana-fleet/cypress/screenshots', + trashAssetsBeforeRuns: false, + video: false, + videosFolder: '../../../target/kibana-fleet/cypress/videos', + viewportHeight: 900, + viewportWidth: 1440, + screenshotOnRunFailure: true, + + e2e: { + baseUrl: 'http://localhost:5601', + + experimentalRunAllSpecs: true, + experimentalMemoryManagement: true, + numTestsKeptInMemory: 3, + + specPattern: './cypress/e2e/space_awareness/**/*.cy.ts', + supportFile: './cypress/support/e2e.ts', + + setupNodeEvents(on, config) { + // eslint-disable-next-line @typescript-eslint/no-var-requires, @kbn/imports/no_boundary_crossing + return require('./cypress/plugins')(on, config); + }, + }, +}); diff --git a/x-pack/plugins/fleet/cypress.config.ts b/x-pack/plugins/fleet/cypress.config.ts index e4a5ad96938d6..2082142e23d7f 100644 --- a/x-pack/plugins/fleet/cypress.config.ts +++ b/x-pack/plugins/fleet/cypress.config.ts @@ -39,6 +39,7 @@ export default defineCypressConfig({ specPattern: './cypress/e2e/**/*.cy.ts', supportFile: './cypress/support/e2e.ts', + excludeSpecPattern: './cypress/e2e/space_awareness/**/*.cy.ts', setupNodeEvents(on, config) { // eslint-disable-next-line @typescript-eslint/no-var-requires, @kbn/imports/no_boundary_crossing diff --git a/x-pack/plugins/fleet/cypress/e2e/integrations_automatic_import.cy.ts b/x-pack/plugins/fleet/cypress/e2e/integrations_automatic_import.cy.ts new file mode 100644 index 0000000000000..e2454cb1dcf77 --- /dev/null +++ b/x-pack/plugins/fleet/cypress/e2e/integrations_automatic_import.cy.ts @@ -0,0 +1,115 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { deleteIntegrations } from '../tasks/integrations'; +import { + UPLOAD_PACKAGE_LINK, + ASSISTANT_BUTTON, + TECH_PREVIEW_BADGE, + CREATE_INTEGRATION_LANDING_PAGE, + BUTTON_FOOTER_NEXT, + INTEGRATION_TITLE_INPUT, + INTEGRATION_DESCRIPTION_INPUT, + DATASTREAM_TITLE_INPUT, + DATASTREAM_DESCRIPTION_INPUT, + DATASTREAM_NAME_INPUT, + DATA_COLLECTION_METHOD_INPUT, + LOGS_SAMPLE_FILE_PICKER, + EDIT_PIPELINE_BUTTON, + SAVE_PIPELINE_BUTTON, + VIEW_INTEGRATION_BUTTON, + INTEGRATION_SUCCESS_SECTION, + SAVE_ZIP_BUTTON, +} from '../screens/integrations_automatic_import'; +import { cleanupAgentPolicies } from '../tasks/cleanup'; +import { login, logout } from '../tasks/login'; +import { createBedrockConnector, deleteConnectors } from '../tasks/api_calls/connectors'; +import { + ecsResultsForJson, + categorizationResultsForJson, + relatedResultsForJson, +} from '../tasks/api_calls/graph_results'; + +describe('Add Integration - Automatic Import', () => { + beforeEach(() => { + login(); + + cleanupAgentPolicies(); + deleteIntegrations(); + + // Create a mock connector + deleteConnectors(); + createBedrockConnector(); + // Mock API Responses + cy.intercept('POST', '/api/integration_assistant/ecs', { + statusCode: 200, + body: { + results: ecsResultsForJson, + }, + }); + cy.intercept('POST', '/api/integration_assistant/categorization', { + statusCode: 200, + body: { + results: categorizationResultsForJson, + }, + }); + cy.intercept('POST', '/api/integration_assistant/related', { + statusCode: 200, + body: { + results: relatedResultsForJson, + }, + }); + }); + + afterEach(() => { + deleteConnectors(); + cleanupAgentPolicies(); + deleteIntegrations(); + logout(); + }); + + it('should create an integration', () => { + cy.visit(CREATE_INTEGRATION_LANDING_PAGE); + + cy.getBySel(ASSISTANT_BUTTON).should('exist'); + cy.getBySel(UPLOAD_PACKAGE_LINK).should('exist'); + cy.getBySel(TECH_PREVIEW_BADGE).should('exist'); + + // Create Integration Assistant Page + cy.getBySel(ASSISTANT_BUTTON).click(); + cy.getBySel(BUTTON_FOOTER_NEXT).click(); + + // Integration details Page + cy.getBySel(INTEGRATION_TITLE_INPUT).type('Test Integration'); + cy.getBySel(INTEGRATION_DESCRIPTION_INPUT).type('Test Integration Description'); + cy.getBySel(BUTTON_FOOTER_NEXT).click(); + + // Datastream details page + cy.getBySel(DATASTREAM_TITLE_INPUT).type('Audit'); + cy.getBySel(DATASTREAM_DESCRIPTION_INPUT).type('Test Datastream Description'); + cy.getBySel(DATASTREAM_NAME_INPUT).type('audit'); + cy.getBySel(DATA_COLLECTION_METHOD_INPUT).type('file stream'); + cy.get('body').click(0, 0); + + // Select sample logs file and Analyze logs + cy.fixture('teleport.ndjson', null).as('myFixture'); + cy.getBySel(LOGS_SAMPLE_FILE_PICKER).selectFile('@myFixture'); + cy.getBySel(BUTTON_FOOTER_NEXT).click(); + + // Edit Pipeline + cy.getBySel(EDIT_PIPELINE_BUTTON).click(); + cy.getBySel(SAVE_PIPELINE_BUTTON).click(); + + // Deploy + cy.getBySel(BUTTON_FOOTER_NEXT).click(); + cy.getBySel(INTEGRATION_SUCCESS_SECTION).should('exist'); + cy.getBySel(SAVE_ZIP_BUTTON).should('exist'); + + // View Integration + cy.getBySel(VIEW_INTEGRATION_BUTTON).click(); + }); +}); diff --git a/x-pack/plugins/fleet/cypress/e2e/privileges_integrations_automatic_import.cy.ts b/x-pack/plugins/fleet/cypress/e2e/privileges_integrations_automatic_import.cy.ts new file mode 100644 index 0000000000000..29eaab7eaca0a --- /dev/null +++ b/x-pack/plugins/fleet/cypress/e2e/privileges_integrations_automatic_import.cy.ts @@ -0,0 +1,159 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import type { User } from '../tasks/privileges'; +import { + deleteUsersAndRoles, + getIntegrationsAutoImportRole, + createUsersAndRoles, + AutomaticImportConnectorNoneUser, + AutomaticImportConnectorNoneRole, + AutomaticImportConnectorAllUser, + AutomaticImportConnectorAllRole, + AutomaticImportConnectorReadUser, + AutomaticImportConnectorReadRole, +} from '../tasks/privileges'; +import { login, loginWithUserAndWaitForPage, logout } from '../tasks/login'; +import { + ASSISTANT_BUTTON, + CONNECTOR_BEDROCK, + CONNECTOR_GEMINI, + CONNECTOR_OPENAI, + CREATE_INTEGRATION_ASSISTANT, + CREATE_INTEGRATION_LANDING_PAGE, + CREATE_INTEGRATION_UPLOAD, + MISSING_PRIVILEGES, + UPLOAD_PACKAGE_LINK, +} from '../screens/integrations_automatic_import'; + +describe('When the user does not have enough previleges for Integrations', () => { + const runs = [ + { fleetRole: 'read', integrationsRole: 'read' }, + { fleetRole: 'read', integrationsRole: 'all' }, + { fleetRole: 'all', integrationsRole: 'read' }, + ]; + + runs.forEach(function (run) { + describe(`When the user has '${run.fleetRole}' role for fleet and '${run.integrationsRole}' role for Integrations`, () => { + const automaticImportIntegrRole = getIntegrationsAutoImportRole({ + fleetv2: [run.fleetRole], // fleet + fleet: [run.integrationsRole], // integrations + }); + const AutomaticImportIntegrUser: User = { + username: 'automatic_import_integrations_read_user', + password: 'password', + roles: [automaticImportIntegrRole.name], + }; + + before(() => { + createUsersAndRoles([AutomaticImportIntegrUser], [automaticImportIntegrRole]); + }); + + beforeEach(() => { + login(); + }); + + afterEach(() => { + logout(); + }); + + after(() => { + deleteUsersAndRoles([AutomaticImportIntegrUser], [automaticImportIntegrRole]); + }); + + it('Create Assistant is not accessible if user has read role in integrations', () => { + loginWithUserAndWaitForPage(CREATE_INTEGRATION_ASSISTANT, AutomaticImportIntegrUser); + cy.getBySel(MISSING_PRIVILEGES).should('exist'); + }); + + it('Create upload is not accessible if user has read role in integrations', () => { + loginWithUserAndWaitForPage(CREATE_INTEGRATION_UPLOAD, AutomaticImportIntegrUser); + cy.getBySel(MISSING_PRIVILEGES).should('exist'); + }); + }); + }); +}); + +describe('When the user has All permissions for Integrations and No permissions for actions', () => { + before(() => { + createUsersAndRoles([AutomaticImportConnectorNoneUser], [AutomaticImportConnectorNoneRole]); + }); + + beforeEach(() => { + login(); + }); + + afterEach(() => { + logout(); + }); + + after(() => { + deleteUsersAndRoles([AutomaticImportConnectorNoneUser], [AutomaticImportConnectorNoneRole]); + }); + + it('Create Assistant is not accessible but upload is accessible', () => { + loginWithUserAndWaitForPage(CREATE_INTEGRATION_LANDING_PAGE, AutomaticImportConnectorNoneUser); + cy.getBySel(ASSISTANT_BUTTON).should('not.exist'); + cy.getBySel(UPLOAD_PACKAGE_LINK).should('exist'); + }); +}); + +describe('When the user has All permissions for Integrations and read permissions for actions', () => { + before(() => { + createUsersAndRoles([AutomaticImportConnectorReadUser], [AutomaticImportConnectorReadRole]); + }); + + beforeEach(() => { + login(); + }); + + afterEach(() => { + logout(); + }); + + after(() => { + deleteUsersAndRoles([AutomaticImportConnectorReadUser], [AutomaticImportConnectorReadRole]); + }); + + it('Create Assistant is not accessible but upload is accessible', () => { + loginWithUserAndWaitForPage(CREATE_INTEGRATION_LANDING_PAGE, AutomaticImportConnectorReadUser); + cy.getBySel(ASSISTANT_BUTTON).should('exist'); + cy.getBySel(UPLOAD_PACKAGE_LINK).should('exist'); + }); + + it('Create Assistant is accessible but execute connector is not accessible', () => { + loginWithUserAndWaitForPage(CREATE_INTEGRATION_ASSISTANT, AutomaticImportConnectorReadUser); + cy.getBySel(CONNECTOR_BEDROCK).should('not.exist'); + cy.getBySel(CONNECTOR_OPENAI).should('not.exist'); + cy.getBySel(CONNECTOR_GEMINI).should('not.exist'); + }); +}); + +describe('When the user has All permissions for Integrations and All permissions for actions', () => { + before(() => { + createUsersAndRoles([AutomaticImportConnectorAllUser], [AutomaticImportConnectorAllRole]); + }); + + beforeEach(() => { + login(); + }); + + afterEach(() => { + logout(); + }); + + after(() => { + deleteUsersAndRoles([AutomaticImportConnectorAllUser], [AutomaticImportConnectorAllRole]); + }); + + it('Create Assistant is not accessible but upload is accessible', () => { + loginWithUserAndWaitForPage(CREATE_INTEGRATION_ASSISTANT, AutomaticImportConnectorAllUser); + cy.getBySel(CONNECTOR_BEDROCK).should('exist'); + cy.getBySel(CONNECTOR_OPENAI).should('exist'); + cy.getBySel(CONNECTOR_GEMINI).should('exist'); + }); +}); diff --git a/x-pack/plugins/fleet/cypress/e2e/space_awareness/policies.cy.ts b/x-pack/plugins/fleet/cypress/e2e/space_awareness/policies.cy.ts new file mode 100644 index 0000000000000..8975de388248c --- /dev/null +++ b/x-pack/plugins/fleet/cypress/e2e/space_awareness/policies.cy.ts @@ -0,0 +1,75 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { + ADD_AGENT_POLICY_BTN, + AGENT_POLICIES_TABLE, + AGENT_POLICY_CREATE_AGENT_POLICY_NAME_FIELD, + AGENT_POLICY_DETAILS_PAGE, + AGENT_POLICY_FLYOUT_CREATE_BUTTON, + AGENT_POLICY_SYSTEM_MONITORING_CHECKBOX, +} from '../../screens/fleet'; +import { login } from '../../tasks/login'; +import { createSpaces, enableSpaceAwareness } from '../../tasks/spaces'; +import { cleanupAgentPolicies } from '../../tasks/cleanup'; + +describe('Space aware policies creation', { testIsolation: false }, () => { + before(() => { + enableSpaceAwareness(); + createSpaces(); + cleanupAgentPolicies(); + cleanupAgentPolicies('test'); + login(); + }); + + beforeEach(() => { + cy.intercept('GET', /\/api\/fleet\/agent_policies/).as('getAgentPolicies'); + cy.intercept('GET', /\/internal\/fleet\/agent_policies_spaces/).as('getAgentPoliciesSpaces'); + }); + + const POLICY_NAME = `Policy 1 space test`; + const NO_AGENT_POLICIES = 'No agent policies'; + it('should allow to create an agent policy in the test space', () => { + cy.visit('/s/test/app/fleet/policies'); + + cy.getBySel(ADD_AGENT_POLICY_BTN).click(); + cy.getBySel(AGENT_POLICY_CREATE_AGENT_POLICY_NAME_FIELD).type(POLICY_NAME); + cy.getBySel(AGENT_POLICY_SYSTEM_MONITORING_CHECKBOX).uncheck(); + + cy.getBySel(AGENT_POLICY_FLYOUT_CREATE_BUTTON).click(); + cy.getBySel(AGENT_POLICIES_TABLE).contains(POLICY_NAME); + }); + + it('the created policy should not be visible in the default space', () => { + cy.visit('/app/fleet/policies'); + cy.wait('@getAgentPolicies'); + cy.getBySel(AGENT_POLICIES_TABLE).contains(NO_AGENT_POLICIES); + }); + + it('should allow to update that policy to belong to both test and default space', () => { + cy.visit('/s/test/app/fleet/policies'); + cy.getBySel(AGENT_POLICIES_TABLE).contains(POLICY_NAME).click(); + + cy.getBySel(AGENT_POLICY_DETAILS_PAGE.SETTINGS_TAB).click(); + cy.wait('@getAgentPoliciesSpaces'); + cy.getBySel(AGENT_POLICY_DETAILS_PAGE.SPACE_SELECTOR_COMBOBOX).click().type('default{enter}'); + + cy.getBySel(AGENT_POLICY_DETAILS_PAGE.SAVE_BUTTON).click(); + }); + + it('the policy should be visible in the test space', () => { + cy.visit('/s/test/app/fleet/policies'); + cy.wait('@getAgentPolicies'); + cy.getBySel(AGENT_POLICIES_TABLE).contains(POLICY_NAME); + }); + + it('the policy should be visible in the default space', () => { + cy.visit('/app/fleet/policies'); + cy.wait('@getAgentPolicies'); + cy.getBySel(AGENT_POLICIES_TABLE).contains(POLICY_NAME); + }); +}); diff --git a/x-pack/plugins/fleet/cypress/fixtures/teleport.ndjson b/x-pack/plugins/fleet/cypress/fixtures/teleport.ndjson new file mode 100644 index 0000000000000..82774ac2297d6 --- /dev/null +++ b/x-pack/plugins/fleet/cypress/fixtures/teleport.ndjson @@ -0,0 +1 @@ +{"ei":0,"event":"cert.create","uid":"efd326fc-dd13-4df8-acef-3102c2d717d3","code":"TC000I","time":"2024-02-24T06:56:50.648137154Z"} \ No newline at end of file diff --git a/x-pack/plugins/fleet/cypress/screens/fleet.ts b/x-pack/plugins/fleet/cypress/screens/fleet.ts index 4e1a0ac0f7e19..0bd449652a800 100644 --- a/x-pack/plugins/fleet/cypress/screens/fleet.ts +++ b/x-pack/plugins/fleet/cypress/screens/fleet.ts @@ -46,6 +46,8 @@ export const AGENT_POLICY_CREATE_AGENT_POLICY_NAME_FIELD = 'createAgentPolicyNam export const AGENT_POLICIES_FLYOUT_ADVANCED_DEFAULT_NAMESPACE_HEADER = 'defaultNamespaceHeader'; export const AGENT_POLICY_FLYOUT_CREATE_BUTTON = 'createAgentPolicyFlyoutBtn'; +export const AGENT_POLICIES_TABLE = 'agentPoliciesTable'; + export const ENROLLMENT_TOKENS = { CREATE_TOKEN_BUTTON: 'createEnrollmentTokenButton', CREATE_TOKEN_MODAL_NAME_FIELD: 'createEnrollmentTokenNameField', @@ -241,4 +243,7 @@ export const API_KEYS = { export const AGENT_POLICY_DETAILS_PAGE = { ADD_AGENT_LINK: 'addAgentLink', + SETTINGS_TAB: 'agentPolicySettingsTab', + SPACE_SELECTOR_COMBOBOX: 'spaceSelectorComboBox', + SAVE_BUTTON: 'agentPolicyDetailsSaveButton', }; diff --git a/x-pack/plugins/fleet/cypress/screens/integrations_automatic_import.ts b/x-pack/plugins/fleet/cypress/screens/integrations_automatic_import.ts new file mode 100644 index 0000000000000..e549f88294a3b --- /dev/null +++ b/x-pack/plugins/fleet/cypress/screens/integrations_automatic_import.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. + */ + +export const UPLOAD_PACKAGE_LINK = 'uploadPackageLink'; +export const ASSISTANT_BUTTON = 'assistantButton'; +export const TECH_PREVIEW_BADGE = 'techPreviewBadge'; +export const MISSING_PRIVILEGES = 'missingPrivilegesCallOut'; + +export const CONNECTOR_BEDROCK = 'actionType-.bedrock'; +export const CONNECTOR_OPENAI = 'actionType-.gen-ai'; +export const CONNECTOR_GEMINI = 'actionType-.gemini'; + +export const BUTTON_FOOTER_NEXT = 'buttonsFooter-nextButton'; + +export const INTEGRATION_TITLE_INPUT = 'integrationTitleInput'; +export const INTEGRATION_DESCRIPTION_INPUT = 'integrationDescriptionInput'; +export const DATASTREAM_TITLE_INPUT = 'dataStreamTitleInput'; +export const DATASTREAM_DESCRIPTION_INPUT = 'dataStreamDescriptionInput'; +export const DATASTREAM_NAME_INPUT = 'dataStreamNameInput'; +export const DATA_COLLECTION_METHOD_INPUT = 'dataCollectionMethodInput'; +export const LOGS_SAMPLE_FILE_PICKER = 'logsSampleFilePicker'; + +export const EDIT_PIPELINE_BUTTON = 'editPipelineButton'; +export const SAVE_PIPELINE_BUTTON = 'savePipelineButton'; +export const VIEW_INTEGRATION_BUTTON = 'viewIntegrationButton'; +export const INTEGRATION_SUCCESS_SECTION = 'integrationSuccessSection'; +export const SAVE_ZIP_BUTTON = 'saveZipButton'; + +export const CREATE_INTEGRATION_LANDING_PAGE = '/app/integrations/create'; +export const CREATE_INTEGRATION_ASSISTANT = '/app/integrations/create/assistant'; +export const CREATE_INTEGRATION_UPLOAD = '/app/integrations/create/upload'; diff --git a/x-pack/plugins/fleet/cypress/tasks/api_calls/connectors.ts b/x-pack/plugins/fleet/cypress/tasks/api_calls/connectors.ts new file mode 100644 index 0000000000000..230fdcd124562 --- /dev/null +++ b/x-pack/plugins/fleet/cypress/tasks/api_calls/connectors.ts @@ -0,0 +1,88 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import type { AllConnectorsResponse } from '@kbn/actions-plugin/common/routes/connector/response'; + +import { v4 as uuidv4 } from 'uuid'; + +import { API_AUTH, COMMON_API_HEADERS } from '../common'; + +export const bedrockId = uuidv4(); +export const azureId = uuidv4(); + +// Replaces request - adds baseline authentication + global headers +export const request = ({ + headers, + ...options +}: Partial): Cypress.Chainable> => { + return cy.request({ + auth: API_AUTH, + headers: { ...COMMON_API_HEADERS, ...headers }, + ...options, + }); +}; +export const INTERNAL_CLOUD_CONNECTORS = ['Elastic-Cloud-SMTP']; + +export const getConnectors = () => + request({ + method: 'GET', + url: 'api/actions/connectors', + }); + +export const createConnector = (connector: Record, id: string) => + cy.request({ + method: 'POST', + url: `/api/actions/connector/${id}`, + body: connector, + headers: { 'kbn-xsrf': 'cypress-creds', 'x-elastic-internal-origin': 'security-solution' }, + }); + +export const deleteConnectors = () => { + getConnectors().then(($response) => { + if ($response.body.length > 0) { + const ids = $response.body.map((connector) => { + return connector.id; + }); + ids.forEach((id) => { + if (!INTERNAL_CLOUD_CONNECTORS.includes(id)) { + request({ + method: 'DELETE', + url: `api/actions/connector/${id}`, + }); + } + }); + } + }); +}; + +export const azureConnectorAPIPayload = { + connector_type_id: '.gen-ai', + secrets: { + apiKey: '123', + }, + config: { + apiUrl: + 'https://goodurl.com/openai/deployments/good-gpt4o/chat/completions?api-version=2024-02-15-preview', + apiProvider: 'Azure OpenAI', + }, + name: 'Azure OpenAI cypress test e2e connector', +}; + +export const bedrockConnectorAPIPayload = { + connector_type_id: '.bedrock', + secrets: { + accessKey: '123', + secret: '123', + }, + config: { + apiUrl: 'https://bedrock.com', + }, + name: 'Bedrock cypress test e2e connector', +}; + +export const createAzureConnector = () => createConnector(azureConnectorAPIPayload, azureId); +export const createBedrockConnector = () => createConnector(bedrockConnectorAPIPayload, bedrockId); diff --git a/x-pack/plugins/fleet/cypress/tasks/api_calls/graph_results.ts b/x-pack/plugins/fleet/cypress/tasks/api_calls/graph_results.ts new file mode 100644 index 0000000000000..3276b6ecf055f --- /dev/null +++ b/x-pack/plugins/fleet/cypress/tasks/api_calls/graph_results.ts @@ -0,0 +1,531 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor 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 ecsResultsForJson = { + mapping: { + teleport2: { + audit: { + ei: null, + event: { + target: 'event.action', + confidence: 0.9, + type: 'string', + date_formats: [], + }, + uid: { + target: 'event.id', + confidence: 0.95, + type: 'string', + date_formats: [], + }, + code: { + target: 'event.code', + confidence: 0.9, + type: 'string', + date_formats: [], + }, + }, + }, + }, + pipeline: { + description: 'Pipeline to process teleport2 audit logs', + processors: [ + { + set: { + field: 'ecs.version', + tag: 'set_ecs_version', + value: '8.11.0', + }, + }, + { + remove: { + field: 'message', + ignore_missing: true, + tag: 'remove_message', + }, + }, + { + json: { + field: 'event.original', + tag: 'json_original', + target_field: 'teleport2.audit', + }, + }, + { + rename: { + field: 'teleport2.audit.event', + target_field: 'event.action', + ignore_missing: true, + }, + }, + { + script: { + description: 'Ensures the date processor does not receive an array value.', + tag: 'script_convert_array_to_string', + lang: 'painless', + source: + 'if (ctx.teleport2?.audit?.time != null &&\n ctx.teleport2.audit.time instanceof ArrayList){\n ctx.teleport2.audit.time = ctx.teleport2.audit.time[0];\n}\n', + }, + }, + { + date: { + field: 'teleport2.audit.time', + target_field: 'event.start', + formats: ["yyyy-MM-dd'T'HH:mm:ss.SSSSSSSSS'Z'", 'ISO8601'], + tag: 'date_processor_teleport2.audit.time', + if: 'ctx.teleport2?.audit?.time != null', + }, + }, + { + set: { + field: 'event.kind', + value: 'pipeline_error', + }, + }, + ], + }, +}; + +export const categorizationResultsForJson = { + docs: [ + { + ecs: { + version: '8.11.0', + }, + teleport2: { + audit: { + cert_type: 'user', + time: '2024-02-24T06:56:50.648137154Z', + ei: 0, + identity: { + expires: '2024-02-24T06:56:50.648137154Z', + traits: { + logins: ['root', 'ubuntu', 'ec2-user'], + }, + private_key_policy: 'none', + teleport_cluster: 'teleport.com', + prev_identity_expires: '0001-01-01T00:00:00Z', + route_to_cluster: 'teleport.com', + logins: ['root', 'ubuntu', 'ec2-user', '-teleport-internal-join'], + }, + }, + }, + organization: { + name: 'teleport.com', + }, + source: { + ip: '1.2.3.4', + }, + event: { + code: 'TC000I', + start: '2024-02-24T06:56:50.648Z', + action: 'cert.create', + end: '0001-01-01T00:00:00.000Z', + id: 'efd326fc-dd13-4df8-acef-3102c2d717d3', + category: ['iam', 'authentication'], + type: ['creation', 'start'], + }, + user: { + name: 'teleport-admin', + changes: { + name: '2024-02-24T06:56:50.648Z', + }, + roles: ['access', 'editor'], + }, + tags: [ + '_geoip_database_unavailable_GeoLite2-City.mmdb', + '_geoip_database_unavailable_GeoLite2-ASN.mmdb', + '_geoip_database_unavailable_GeoLite2-City.mmdb', + '_geoip_database_unavailable_GeoLite2-ASN.mmdb', + ], + }, + ], + pipeline: { + description: 'Pipeline to process teleport2 audit logs', + processors: [ + { + set: { + field: 'ecs.version', + tag: 'set_ecs_version', + value: '8.11.0', + }, + }, + { + remove: { + field: 'message', + ignore_missing: true, + tag: 'remove_message', + }, + }, + { + json: { + field: 'event.original', + tag: 'json_original', + target_field: 'teleport2.audit', + }, + }, + { + rename: { + field: 'teleport2.audit.event', + target_field: 'event.action', + ignore_missing: true, + }, + }, + { + script: { + description: 'Ensures the date processor does not receive an array value.', + tag: 'script_convert_array_to_string', + lang: 'painless', + source: + 'if (ctx.teleport2?.audit?.time != null &&\n ctx.teleport2.audit.time instanceof ArrayList){\n ctx.teleport2.audit.time = ctx.teleport2.audit.time[0];\n}\n', + }, + }, + { + date: { + field: 'teleport2.audit.time', + target_field: 'event.start', + formats: ["yyyy-MM-dd'T'HH:mm:ss.SSSSSSSSS'Z'", 'ISO8601'], + tag: 'date_processor_teleport2.audit.time', + if: 'ctx.teleport2?.audit?.time != null', + }, + }, + { + set: { + field: 'event.kind', + value: 'pipeline_error', + }, + }, + ], + }, +}; + +export const relatedResultsForJson = { + docs: [ + { + ecs: { + version: '8.11.0', + }, + related: { + user: ['teleport-admin'], + ip: ['1.2.3.4'], + }, + teleport2: { + audit: { + cert_type: 'user', + time: '2024-02-24T06:56:50.648137154Z', + ei: 0, + identity: { + expires: '2024-02-24T06:56:50.648137154Z', + traits: { + logins: ['root', 'ubuntu', 'ec2-user'], + }, + private_key_policy: 'none', + teleport_cluster: 'teleport.com', + prev_identity_expires: '0001-01-01T00:00:00Z', + route_to_cluster: 'teleport.com', + logins: ['root', 'ubuntu', 'ec2-user', '-teleport-internal-join'], + }, + }, + }, + organization: { + name: 'teleport.com', + }, + source: { + ip: '1.2.3.4', + }, + event: { + code: 'TC000I', + start: '2024-02-24T06:56:50.648Z', + action: 'cert.create', + end: '0001-01-01T00:00:00.000Z', + id: 'efd326fc-dd13-4df8-acef-3102c2d717d3', + category: ['iam', 'authentication'], + type: ['creation', 'start'], + }, + user: { + name: 'teleport-admin', + changes: { + name: '2024-02-24T06:56:50.648Z', + }, + roles: ['access', 'editor'], + }, + tags: [ + '_geoip_database_unavailable_GeoLite2-City.mmdb', + '_geoip_database_unavailable_GeoLite2-ASN.mmdb', + '_geoip_database_unavailable_GeoLite2-City.mmdb', + '_geoip_database_unavailable_GeoLite2-ASN.mmdb', + ], + }, + ], + pipeline: { + description: 'Pipeline to process teleport2 audit logs', + processors: [ + { + set: { + tag: 'set_ecs_version', + field: 'ecs.version', + value: '8.11.0', + }, + }, + { + set: { + tag: 'copy_original_message', + field: 'originalMessage', + copy_from: 'message', + }, + }, + { + rename: { + ignore_missing: true, + if: 'ctx.event?.original == null', + tag: 'rename_message', + field: 'originalMessage', + target_field: 'event.original', + }, + }, + { + rename: { + ignore_missing: true, + field: 'teleport2.audit.user', + target_field: 'user.name', + }, + }, + { + rename: { + ignore_missing: true, + field: 'teleport2.audit.login', + target_field: 'user.id', + }, + }, + { + rename: { + ignore_missing: true, + field: 'teleport2.audit.server_hostname', + target_field: 'destination.domain', + }, + }, + { + rename: { + ignore_missing: true, + field: 'teleport2.audit.addr.remote', + target_field: 'source.address', + }, + }, + { + rename: { + ignore_missing: true, + field: 'teleport2.audit.proto', + target_field: 'network.protocol', + }, + }, + { + script: { + tag: 'script_drop_null_empty_values', + description: 'Drops null/empty values recursively.', + lang: 'painless', + source: + 'boolean dropEmptyFields(Object object) {\n if (object == null || object == "") {\n return true;\n } else if (object instanceof Map) {\n ((Map) object).values().removeIf(value -> dropEmptyFields(value));\n return (((Map) object).size() == 0);\n } else if (object instanceof List) {\n ((List) object).removeIf(value -> dropEmptyFields(value));\n return (((List) object).length == 0);\n }\n return false;\n}\ndropEmptyFields(ctx);\n', + }, + }, + { + geoip: { + ignore_missing: true, + tag: 'geoip_source_ip', + field: 'source.ip', + target_field: 'source.geo', + }, + }, + { + geoip: { + ignore_missing: true, + tag: 'geoip_source_asn', + database_file: 'GeoLite2-ASN.mmdb', + field: 'source.ip', + target_field: 'source.as', + properties: ['asn', 'organization_name'], + }, + }, + { + rename: { + ignore_missing: true, + tag: 'rename_source_as_asn', + field: 'source.as.asn', + target_field: 'source.as.number', + }, + }, + { + rename: { + ignore_missing: true, + tag: 'rename_source_as_organization_name', + field: 'source.as.organization_name', + target_field: 'source.as.organization.name', + }, + }, + { + geoip: { + ignore_missing: true, + tag: 'geoip_destination_ip', + field: 'destination.ip', + target_field: 'destination.geo', + }, + }, + { + geoip: { + ignore_missing: true, + tag: 'geoip_destination_asn', + database_file: 'GeoLite2-ASN.mmdb', + field: 'destination.ip', + target_field: 'destination.as', + properties: ['asn', 'organization_name'], + }, + }, + { + rename: { + ignore_missing: true, + tag: 'rename_destination_as_asn', + field: 'destination.as.asn', + target_field: 'destination.as.number', + }, + }, + { + rename: { + ignore_missing: true, + tag: 'rename_destination_as_organization_name', + field: 'destination.as.organization_name', + target_field: 'destination.as.organization.name', + }, + }, + { + append: { + if: "ctx.event?.action == 'cert.create'", + field: 'event.category', + value: ['iam'], + allow_duplicates: false, + }, + }, + { + append: { + if: "ctx.event?.action == 'cert.create'", + field: 'event.type', + value: ['creation'], + allow_duplicates: false, + }, + }, + { + append: { + if: "ctx.event?.action == 'cert.create'", + field: 'event.category', + value: ['authentication'], + allow_duplicates: false, + }, + }, + { + append: { + if: "ctx.event?.action == 'cert.create'", + field: 'event.type', + value: ['start'], + allow_duplicates: false, + }, + }, + { + append: { + if: "ctx.event?.action == 'session.start'", + field: 'event.category', + value: ['session'], + allow_duplicates: false, + }, + }, + { + append: { + if: "ctx.event?.action == 'session.start'", + field: 'event.type', + value: ['start'], + allow_duplicates: false, + }, + }, + { + append: { + if: "ctx.network?.protocol == 'ssh'", + field: 'event.category', + value: ['network'], + allow_duplicates: false, + }, + }, + { + append: { + if: "ctx.network?.protocol == 'ssh'", + field: 'event.type', + value: ['connection', 'start'], + allow_duplicates: false, + }, + }, + { + append: { + field: 'related.ip', + value: '{{{source.ip}}}', + if: 'ctx.source?.ip != null', + allow_duplicates: false, + }, + }, + { + append: { + field: 'related.user', + value: '{{{user.name}}}', + if: 'ctx.user?.name != null', + allow_duplicates: false, + }, + }, + { + append: { + field: 'related.hosts', + value: '{{{destination.domain}}}', + if: 'ctx.destination?.domain != null', + allow_duplicates: false, + }, + }, + { + append: { + field: 'related.user', + value: '{{{user.id}}}', + if: 'ctx.user?.id != null', + allow_duplicates: false, + }, + }, + { + remove: { + ignore_missing: true, + tag: 'remove_fields', + field: ['teleport2.audit.identity.client_ip'], + }, + }, + { + remove: { + ignore_failure: true, + ignore_missing: true, + if: 'ctx?.tags == null || !(ctx.tags.contains("preserve_original_event"))', + tag: 'remove_original_event', + field: 'event.original', + }, + }, + ], + on_failure: [ + { + append: { + field: 'error.message', + value: + 'Processor {{{_ingest.on_failure_processor_type}}} with tag {{{_ingest.on_failure_processor_tag}}} in pipeline {{{_ingest.on_failure_pipeline}}} failed with message: {{{_ingest.on_failure_message}}}', + }, + }, + { + set: { + field: 'event.kind', + value: 'pipeline_error', + }, + }, + ], + }, +}; diff --git a/x-pack/plugins/fleet/cypress/tasks/cleanup.ts b/x-pack/plugins/fleet/cypress/tasks/cleanup.ts index 5e179bc9207f1..bad8743b66bd1 100644 --- a/x-pack/plugins/fleet/cypress/tasks/cleanup.ts +++ b/x-pack/plugins/fleet/cypress/tasks/cleanup.ts @@ -7,18 +7,20 @@ import { request } from './common'; -export function cleanupAgentPolicies() { - request({ url: '/api/fleet/agent_policies' }).then((response: any) => { - response.body.items - .filter((policy: any) => policy.agents === 0) - .forEach((policy: any) => { - request({ - method: 'POST', - url: '/api/fleet/agent_policies/delete', - body: { agentPolicyId: policy.id }, +export function cleanupAgentPolicies(spaceId?: string) { + request({ url: `${spaceId ? `/s/${spaceId}` : ''}/api/fleet/agent_policies` }).then( + (response: any) => { + response.body.items + .filter((policy: any) => policy.agents === 0) + .forEach((policy: any) => { + request({ + method: 'POST', + url: `${spaceId ? `/s/${spaceId}` : ''}/api/fleet/agent_policies/delete`, + body: { agentPolicyId: policy.id }, + }); }); - }); - }); + } + ); } export function unenrollAgent() { diff --git a/x-pack/plugins/fleet/cypress/tasks/common.ts b/x-pack/plugins/fleet/cypress/tasks/common.ts index 250dea07f89d1..de6e117bac4cc 100644 --- a/x-pack/plugins/fleet/cypress/tasks/common.ts +++ b/x-pack/plugins/fleet/cypress/tasks/common.ts @@ -28,6 +28,12 @@ export const COMMON_API_HEADERS = Object.freeze({ 'Elastic-Api-Version': API_VERSIONS.public.v1, }); +export const COMMON_INTERNAL_API_HEADERS = Object.freeze({ + 'kbn-xsrf': 'cypress', + 'x-elastic-internal-origin': 'fleet', + 'Elastic-Api-Version': API_VERSIONS.internal.v1, +}); + // Replaces request - adds baseline authentication + global headers export const request = ({ headers, @@ -40,6 +46,17 @@ export const request = ({ }); }; +export const internalRequest = ({ + headers, + ...options +}: Partial): Cypress.Chainable> => { + return cy.request({ + auth: API_AUTH, + headers: { ...COMMON_INTERNAL_API_HEADERS, ...headers }, + ...options, + }); +}; + /** * For all the new features tours we show in the app, this method disables them * by setting their configs in the local storage. It prevents the tours from appearing diff --git a/x-pack/plugins/fleet/cypress/tasks/privileges.ts b/x-pack/plugins/fleet/cypress/tasks/privileges.ts index 214bd0f14e6e6..876b88ac9d5b5 100644 --- a/x-pack/plugins/fleet/cypress/tasks/privileges.ts +++ b/x-pack/plugins/fleet/cypress/tasks/privileges.ts @@ -8,7 +8,7 @@ import { request } from './common'; import { constructUrlWithUser, getEnvAuth } from './login'; -interface User { +export interface User { username: string; password: string; description?: string; @@ -193,6 +193,117 @@ export const FleetNoneIntegrAllUser: User = { roles: [FleetNoneIntegrAllRole.name], }; +export const getIntegrationsAutoImportRole = (feature: FeaturesPrivileges): Role => ({ + name: 'automatic_import_integrations_read_role', + privileges: { + elasticsearch: { + indices: [ + { + names: ['*'], + privileges: ['all'], + }, + ], + cluster: ['manage_service_account'], + }, + kibana: [ + { + feature, + spaces: ['*'], + }, + ], + }, +}); + +export const AutomaticImportConnectorNoneRole: Role = { + name: 'automatic_import_connectors_none_role', + privileges: { + elasticsearch: { + indices: [ + { + names: ['*'], + privileges: ['all'], + }, + ], + cluster: ['manage_service_account'], + }, + kibana: [ + { + feature: { + fleetv2: ['all'], + fleet: ['all'], + actions: ['none'], + }, + spaces: ['*'], + }, + ], + }, +}; +export const AutomaticImportConnectorNoneUser: User = { + username: 'automatic_import_connectors_none_user', + password: 'password', + roles: [AutomaticImportConnectorNoneRole.name], +}; + +export const AutomaticImportConnectorReadRole: Role = { + name: 'automatic_import_connectors_read_role', + privileges: { + elasticsearch: { + indices: [ + { + names: ['*'], + privileges: ['all'], + }, + ], + cluster: ['manage_service_account'], + }, + kibana: [ + { + feature: { + fleetv2: ['all'], + fleet: ['all'], + actions: ['read'], + }, + spaces: ['*'], + }, + ], + }, +}; +export const AutomaticImportConnectorReadUser: User = { + username: 'automatic_import_connectors_read_user', + password: 'password', + roles: [AutomaticImportConnectorReadRole.name], +}; + +export const AutomaticImportConnectorAllRole: Role = { + name: 'automatic_import_connectors_all_role', + privileges: { + elasticsearch: { + indices: [ + { + names: ['*'], + privileges: ['all'], + }, + ], + cluster: ['manage_service_account'], + }, + kibana: [ + { + feature: { + fleetv2: ['all'], + fleet: ['all'], + actions: ['all'], + }, + spaces: ['*'], + }, + ], + }, +}; +export const AutomaticImportConnectorAllUser: User = { + username: 'automatic_import_connectors_all_user', + password: 'password', + roles: [AutomaticImportConnectorAllRole.name], +}; + export const BuiltInEditorUser: User = { username: 'editor_user', password: 'password', diff --git a/x-pack/plugins/fleet/cypress/tasks/spaces.ts b/x-pack/plugins/fleet/cypress/tasks/spaces.ts new file mode 100644 index 0000000000000..31c1d12a968cb --- /dev/null +++ b/x-pack/plugins/fleet/cypress/tasks/spaces.ts @@ -0,0 +1,38 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { request, internalRequest } from './common'; + +export function enableSpaceAwareness() { + return internalRequest({ + url: '/internal/fleet/enable_space_awareness', + failOnStatusCode: false, + method: 'POST', + }); +} + +export function createSpaces() { + return request({ + url: '/api/spaces/space', + failOnStatusCode: false, + method: 'POST', + body: { + id: 'test', + name: 'Test', + description: 'Test space', + color: '#aabbcc', + initials: 'TE', + disabledFeatures: [], + imageUrl: + 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAD4AAABACAYAAABC6cT1AAAGf0lEQVRoQ+3abYydRRUH8N882xYo0IqagEVjokQJKAiKBjXExC9G/aCkGowCIghCkRcrVSSKIu/FEiqgGL6gBIlAYrAqUTH6hZgQFVEMKlQFfItWoQWhZe8z5uzMLdvbfbkLxb13d+fbvfe588x/zpn/+Z9zJpmnI81T3BaAzzfLL1h8weLzZAcWXH2eGHo7zAWLL1h8nuzAjFw9G1N6Kzq8HnuM36MR8iibF3Fv4q+7cv8yDV6K13bYq2furSP8Ag8ncr/vnSnwRViJT2GfCV7yL1yHGxLb+l3EdM9lluNEnIC9xz+f2ZL4Er6Z2DrdXN3fZwp8CU7OfDHxggle8lTLbQ1nJ/7Z7yKmey5zYGZt4h2IzR8/trRc2PDlxJPTzfVcgJ+CC0wMPOa9F6cm7up3EVM9V9386MxliVdM8GwAv6hh/awCz/w7lY25OtF5ruBz4ZLP42NYNrDAFbC3YPWuILnMAfgq3oaRQQYea/stViV+sgssvjKzLvGySeaaNVfP4d7Btokgvxj/bblgpueuF1hmWcyTCmfE3J3M1lTcv0vMswM88zR+jpw4osu6me8kzkpsfLZWzxyRuabO22buxxOJ12FxnXfWgEe83pB5sOE47BsLymzscOoi7nw2JJfZreUjiUsTyzKPZm5NvBDvSuw268AzNzV8H5/Am+qCnsAXgpgSW2Zq9cyKlksbPlTd+te4quWNieMHBfiNDdciYnwsdI/MaOaWhnMTf54J8CqNj8x8JXFIZltYu+HqlmNT8YSBsHgAPw/vxvlVV4du/s0oaxbxg0TbL/jMni0nNcVjQq7+HZfgtpbzBg342TgQ63AkmsymxBW4IjE6A+D7Vzd/fyWxIM/VuCe+HzTgZ2Jpy/kNJ2FJLmLm24mPJ/42A+Bvrxt4SISwlhsaPodH26LZB8rVA3inwwebsrixJCZzX+KMxI/7AV61eVh3DV6Mx3EOvh4kN6jAg8nfUCXm4d1wE66OyxNPTQc+s3/o/MoXizL3JE5O3F3P/uBZPPF4Zr+Wi5uSO48ZPRdyCwn7YB/A35m5KhWNHox4fcNnIs0ddOCRSBxf8+cQG+Huf0l8NJVYP+nI7NXy2ar4QqIGm69JfKPOE2w/mBavCzwM11R2D+ChsUO7hyUfmwx55qDM1xJvqZ7y08TpifuGBfjeURVJnNIVGpkNiXNS0ds7jcySDitDCCWW56LJ10fRo8sNA+3qXUSZD2CtQlZh9T+1rB7h9oliembflnMbzqgSNZKbKGHdPm7OwXb1CvQ1metSETMpszmzvikCJNh/h5E5PHNl4qga/+/cxqrdeWDYgIe7X5L4cGJPJX2940lOX8pD41FnFnc4riluvQKbK0dcHJFi2IBHNTQSlguru4d2/wPOTNzRA3x5y+U1E1uqWDkETOT026XuUJzx6u7ReLhSYenQ7uHua0fKZmwfmcPqsQjxE5WVONcRxn7X89zgn/EKPMRMxOVQXmP18Mx3q3b/Y/0cQE/IhFtHESMsHFlZ1Ml3CH3DZPHImY+pxcKumNmYirtvqMBfhMuU6s3iqOQkTsMPe1tCQwO8Ajs0lxr7W+vnp1MJc9EgCNd/cy6x+9D4veXmprj5wxMw/3C4egW6zzgZOlYZzfwo3F2J7ael0pJamvlPKgWNKFft1AAcKotXoFEbD7kaoSoQPVKB35+5KHF0lai/rJo+up87jWEE/qqqwY+qrL21LWLm95lPJ16ppKw31XC3PXYPJauPEx7B6BHCgrSizRs18qiaRp8tlN3ueCTYPHH9RNaunjI8Z7wLYpT3jZSCYXQ8e9vTsRE/q+no3XMKeObgGtaintbb/AvXj4JDkNw/5hrwYPfIvlZFUbLn7G5q+eQIN09Vnho6cqvnM/Lt99RixH49wO8K0ZL41WTWHoQzvsNVkOheZqKhEGpsp3SzB+BBtZAYve7uOR9tuTaaB6l0XScdYfEQPpkTUyHEGP+XqyDBzu+NBCITUjNWHynkrbWKOuWFn1xKzqsyx0bdvS78odp0+N503Zao0uCsWuSIDku8/7EO60b41vN5+Ses9BKlTdvd8bhp9EBvJjWJAIn/vxwHe6b3tSk6JFPV4nq85oAOrx555v/x/rh3E6Lo+bnuNS4uB4Cuq0ZfvO8X1rM6q/+vnjLVqZq7v83onttc2oYF4HPJmv1gWbB4P7s0l55ZsPhcsmY/WBYs3s8uzaVn5q3F/wf70mRuBCtbjQAAAABJRU5ErkJggg==', + }, + }).then((response: any) => { + if (response.status !== 200 && response.status !== 409) { + throw new Error(`Failed to create space test`); + } + }); +} diff --git a/x-pack/plugins/fleet/cypress/tsconfig.json b/x-pack/plugins/fleet/cypress/tsconfig.json index ee3dd7cd1e246..5427996c27f17 100644 --- a/x-pack/plugins/fleet/cypress/tsconfig.json +++ b/x-pack/plugins/fleet/cypress/tsconfig.json @@ -3,6 +3,7 @@ "include": [ "**/*", "../cypress.config.ts", + "../cypress.config.space_awareness.ts", "../../../../typings/**/*" ], "exclude": [ @@ -29,5 +30,6 @@ "force": true }, "@kbn/rison", + "@kbn/actions-plugin", ] } diff --git a/x-pack/plugins/fleet/package.json b/x-pack/plugins/fleet/package.json index 3e20162ab1d91..dc0bc6a6bcacb 100644 --- a/x-pack/plugins/fleet/package.json +++ b/x-pack/plugins/fleet/package.json @@ -5,6 +5,10 @@ "private": true, "license": "Elastic License 2.0", "scripts": { + "cypress_space_awareness": "NODE_OPTIONS=--openssl-legacy-provider node ../security_solution/scripts/start_cypress_parallel --config-file ../fleet/cypress.config.space_awareness.ts --ftr-config-file ../../../x-pack/test/fleet_cypress/cli_config.space_awareness", + "cypress_space_awareness:open": "yarn cypress_space_awareness open", + "cypress_space_awareness:run": "yarn cypress_space_awareness run", + "cypress_space_awareness:run:reporter": "yarn cypress_space_awareness run --reporter ../../../node_modules/cypress-multi-reporters --reporter-options configFile=../fleet/cypress/reporter_config.json", "cypress": "NODE_OPTIONS=--openssl-legacy-provider node ../security_solution/scripts/start_cypress_parallel --config-file ../fleet/cypress.config.ts --ftr-config-file ../../../x-pack/test/fleet_cypress/cli_config", "cypress:open": "yarn cypress open", "cypress:run": "yarn cypress run", diff --git a/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/create_package_policy_page/single_page_layout/index.tsx b/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/create_package_policy_page/single_page_layout/index.tsx index 3070b0961ab6d..24f8fa8a04fe5 100644 --- a/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/create_package_policy_page/single_page_layout/index.tsx +++ b/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/create_package_policy_page/single_page_layout/index.tsx @@ -350,7 +350,7 @@ export const CreatePackagePolicySinglePage: CreatePackagePolicyParams = ({ "'package-policy-create' and 'package-policy-replace-define-step' cannot both be registered as UI extensions" ); } - const { isAgentlessEnabled, isAgentlessIntegration } = useAgentless(); + const { isAgentlessIntegration } = useAgentless(); const { handleSetupTechnologyChange, selectedSetupTechnology } = useSetupTechnology({ newAgentPolicy, setNewAgentPolicy, @@ -374,7 +374,7 @@ export const CreatePackagePolicySinglePage: CreatePackagePolicyParams = ({ validationResults={validationResults} isEditPage={false} handleSetupTechnologyChange={handleSetupTechnologyChange} - isAgentlessEnabled={isAgentlessEnabled} + isAgentlessEnabled={isAgentlessIntegration(packageInfo)} /> ) diff --git a/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/details_page/components/package_policies/package_policies_table.tsx b/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/details_page/components/package_policies/package_policies_table.tsx index ed9805fb5f75a..83e18d77f2a06 100644 --- a/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/details_page/components/package_policies/package_policies_table.tsx +++ b/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/details_page/components/package_policies/package_policies_table.tsx @@ -426,7 +426,7 @@ export const PackagePoliciesTable: React.FunctionComponent = ({ ) : ( ), } diff --git a/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/details_page/components/settings/index.tsx b/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/details_page/components/settings/index.tsx index 14dcf6df21b9f..6e4f1e06b45a0 100644 --- a/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/details_page/components/settings/index.tsx +++ b/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/details_page/components/settings/index.tsx @@ -244,6 +244,7 @@ export const SettingsView = memo<{ agentPolicy: AgentPolicy }>( Object.keys(validation).length > 0 || hasAdvancedSettingsErrors } + data-test-subj="agentPolicyDetailsSaveButton" iconType="save" color="primary" fill diff --git a/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/details_page/index.tsx b/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/details_page/index.tsx index 0643ac82634d9..7f4c1a3b91ead 100644 --- a/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/details_page/index.tsx +++ b/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/details_page/index.tsx @@ -90,6 +90,7 @@ export const AgentPolicyDetailsPage: React.FunctionComponent = () => { name: i18n.translate('xpack.fleet.policyDetails.subTabs.settingsTabText', { defaultMessage: 'Settings', }), + 'data-test-subj': 'agentPolicySettingsTab', href: getHref('policy_details', { policyId, tabId: 'settings' }), isSelected: tabId === 'settings', }, diff --git a/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/edit_package_policy_page/index.tsx b/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/edit_package_policy_page/index.tsx index e448d1376b2fe..6157f09968680 100644 --- a/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/edit_package_policy_page/index.tsx +++ b/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/edit_package_policy_page/index.tsx @@ -103,7 +103,7 @@ export const EditPackagePolicyForm = memo<{ } = useConfig(); const { getHref } = useLink(); const { canUseMultipleAgentPolicies } = useMultipleAgentPolicies(); - const { isAgentlessAgentPolicy } = useAgentless(); + const { isAgentlessAgentPolicy, isAgentlessIntegration } = useAgentless(); const { // data agentPolicies: existingAgentPolicies, @@ -130,9 +130,10 @@ export const EditPackagePolicyForm = memo<{ const hasAgentlessAgentPolicy = useMemo( () => existingAgentPolicies.length === 1 - ? existingAgentPolicies.some((policy) => isAgentlessAgentPolicy(policy)) + ? existingAgentPolicies.some((policy) => isAgentlessAgentPolicy(policy)) && + isAgentlessIntegration(packageInfo) : false, - [existingAgentPolicies, isAgentlessAgentPolicy] + [existingAgentPolicies, isAgentlessAgentPolicy, packageInfo, isAgentlessIntegration] ); const canWriteIntegrationPolicies = useAuthz().integrations.writeIntegrationPolicies; diff --git a/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/list_page/index.tsx b/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/list_page/index.tsx index dcbd29c1ef74e..2682a5239071d 100644 --- a/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/list_page/index.tsx +++ b/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/list_page/index.tsx @@ -356,6 +356,7 @@ export const AgentPolicyListPage: React.FunctionComponent<{}> = () => { loading={isLoading} + data-test-subj="agentPoliciesTable" noItemsMessage={ isLoading ? ( ), } diff --git a/x-pack/plugins/fleet/server/services/agents/agentless_agent.test.ts b/x-pack/plugins/fleet/server/services/agents/agentless_agent.test.ts index e55b883e80029..e7db96812749b 100644 --- a/x-pack/plugins/fleet/server/services/agents/agentless_agent.test.ts +++ b/x-pack/plugins/fleet/server/services/agents/agentless_agent.test.ts @@ -370,6 +370,82 @@ describe('Agentless Agent service', () => { ); }); + it('should delete agentless agent for ESS', async () => { + const returnValue = { + id: 'mocked', + }; + + (axios as jest.MockedFunction).mockResolvedValueOnce(returnValue); + jest.spyOn(appContextService, 'getConfig').mockReturnValue({ + agentless: { + enabled: true, + api: { + url: 'http://api.agentless.com', + tls: { + certificate: '/path/to/cert', + key: '/path/to/key', + ca: '/path/to/ca', + }, + }, + }, + } as any); + jest.spyOn(appContextService, 'getCloud').mockReturnValue({ isCloudEnabled: true } as any); + + const deleteAgentlessAgentReturnValue = await agentlessAgentService.deleteAgentlessAgent( + 'mocked-agentless-agent-policy-id' + ); + + expect(axios).toHaveBeenCalledTimes(1); + expect(deleteAgentlessAgentReturnValue).toEqual(returnValue); + expect(axios).toHaveBeenCalledWith( + expect.objectContaining({ + headers: expect.anything(), + httpsAgent: expect.anything(), + method: 'DELETE', + url: 'http://api.agentless.com/api/v1/ess/deployments/mocked-agentless-agent-policy-id', + }) + ); + }); + + it('should delete agentless agent for serverless', async () => { + const returnValue = { + id: 'mocked', + }; + + (axios as jest.MockedFunction).mockResolvedValueOnce(returnValue); + jest.spyOn(appContextService, 'getConfig').mockReturnValue({ + agentless: { + enabled: true, + api: { + url: 'http://api.agentless.com', + tls: { + certificate: '/path/to/cert', + key: '/path/to/key', + ca: '/path/to/ca', + }, + }, + }, + } as any); + jest + .spyOn(appContextService, 'getCloud') + .mockReturnValue({ isCloudEnabled: true, isServerlessEnabled: true } as any); + + const deleteAgentlessAgentReturnValue = await agentlessAgentService.deleteAgentlessAgent( + 'mocked-agentless-agent-policy-id' + ); + + expect(axios).toHaveBeenCalledTimes(1); + expect(deleteAgentlessAgentReturnValue).toEqual(returnValue); + expect(axios).toHaveBeenCalledWith( + expect.objectContaining({ + headers: expect.anything(), + httpsAgent: expect.anything(), + method: 'DELETE', + url: 'http://api.agentless.com/api/v1/serverless/deployments/mocked-agentless-agent-policy-id', + }) + ); + }); + it('should redact sensitive information from debug logs', async () => { const returnValue = { id: 'mocked', diff --git a/x-pack/plugins/fleet/server/services/agents/agentless_agent.ts b/x-pack/plugins/fleet/server/services/agents/agentless_agent.ts index 3bf21c3bec0d1..617f3db7849f4 100644 --- a/x-pack/plugins/fleet/server/services/agents/agentless_agent.ts +++ b/x-pack/plugins/fleet/server/services/agents/agentless_agent.ts @@ -25,11 +25,7 @@ import { appContextService } from '../app_context'; import { listEnrollmentApiKeys } from '../api_keys'; import { listFleetServerHosts } from '../fleet_server_host'; import type { AgentlessConfig } from '../utils/agentless'; -import { - prependAgentlessApiBasePathToEndpoint, - isAgentlessApiEnabled, - getDeletionEndpointPath, -} from '../utils/agentless'; +import { prependAgentlessApiBasePathToEndpoint, isAgentlessApiEnabled } from '../utils/agentless'; class AgentlessAgentService { public async createAgentlessAgent( @@ -188,7 +184,10 @@ class AgentlessAgentService { const agentlessConfig = appContextService.getConfig()?.agentless; const tlsConfig = this.createTlsConfig(agentlessConfig); const requestConfig = { - url: getDeletionEndpointPath(agentlessConfig, `/deployments/${agentlessPolicyId}`), + url: prependAgentlessApiBasePathToEndpoint( + agentlessConfig, + `/deployments/${agentlessPolicyId}` + ), method: 'DELETE', headers: { 'Content-type': 'application/json', diff --git a/x-pack/plugins/fleet/server/services/utils/agentless.ts b/x-pack/plugins/fleet/server/services/utils/agentless.ts index c85e9cc991a6c..4c27d583d9a79 100644 --- a/x-pack/plugins/fleet/server/services/utils/agentless.ts +++ b/x-pack/plugins/fleet/server/services/utils/agentless.ts @@ -50,10 +50,3 @@ export const prependAgentlessApiBasePathToEndpoint = ( : AGENTLESS_ESS_API_BASE_PATH; return `${agentlessConfig.api.url}${endpointPrefix}${endpoint}`; }; - -export const getDeletionEndpointPath = ( - agentlessConfig: FleetConfigType['agentless'], - endpoint: AgentlessApiEndpoints -) => { - return `${agentlessConfig.api.url}${AGENTLESS_ESS_API_BASE_PATH}${endpoint}`; -}; diff --git a/x-pack/plugins/fleet/server/types/rest_spec/epm.ts b/x-pack/plugins/fleet/server/types/rest_spec/epm.ts index 2dc9606a5432d..f08ccd9ff1248 100644 --- a/x-pack/plugins/fleet/server/types/rest_spec/epm.ts +++ b/x-pack/plugins/fleet/server/types/rest_spec/epm.ts @@ -163,7 +163,13 @@ export const PackageInfoSchema = schema release: schema.maybe( schema.oneOf([schema.literal('ga'), schema.literal('beta'), schema.literal('experimental')]) ), - type: schema.maybe(schema.oneOf([schema.literal('integration'), schema.literal('input')])), + type: schema.maybe( + schema.oneOf([ + schema.literal('integration'), + schema.literal('input'), + schema.literal('content'), + ]) + ), path: schema.maybe(schema.string()), download: schema.maybe(schema.string()), internal: schema.maybe(schema.boolean()), @@ -192,6 +198,11 @@ export const PackageInfoSchema = schema format_version: schema.maybe(schema.string()), vars: schema.maybe(schema.arrayOf(schema.recordOf(schema.string(), schema.any()))), latestVersion: schema.maybe(schema.string()), + discovery: schema.maybe( + schema.object({ + fields: schema.maybe(schema.arrayOf(schema.object({ name: schema.string() }))), + }) + ), }) // sometimes package list response contains extra properties, e.g. installed_kibana .extendsDeep({ diff --git a/x-pack/plugins/fleet/tsconfig.json b/x-pack/plugins/fleet/tsconfig.json index 7f7fc8e64829e..7ae2402aa6cb6 100644 --- a/x-pack/plugins/fleet/tsconfig.json +++ b/x-pack/plugins/fleet/tsconfig.json @@ -5,6 +5,7 @@ }, "exclude": [ "cypress.config.ts", + "cypress.config.space_awareness.ts", "target/**/*", ], "include": [ @@ -17,6 +18,7 @@ "scripts/**/*", "package.json", "cypress.config.ts", + "cypress.config.space_awareness.ts", "../../../typings/**/*" ], "kbn_references": [ diff --git a/x-pack/plugins/index_lifecycle_management/server/plugin.ts b/x-pack/plugins/index_lifecycle_management/server/plugin.ts index 0d88acbaaa4ff..a5002cd36da44 100644 --- a/x-pack/plugins/index_lifecycle_management/server/plugin.ts +++ b/x-pack/plugins/index_lifecycle_management/server/plugin.ts @@ -27,7 +27,6 @@ const indexLifecycleDataEnricher = async ( const { indices: ilmIndicesData } = await client.asCurrentUser.ilm.explainLifecycle({ index: '*,.*', - only_managed: true, }); return indicesList.map((index: Index) => { return { diff --git a/x-pack/plugins/integration_assistant/public/common/components/authorization/missing_privileges_description.tsx b/x-pack/plugins/integration_assistant/public/common/components/authorization/missing_privileges_description.tsx index 15365aeb3a08e..ccc65a2e49f0e 100644 --- a/x-pack/plugins/integration_assistant/public/common/components/authorization/missing_privileges_description.tsx +++ b/x-pack/plugins/integration_assistant/public/common/components/authorization/missing_privileges_description.tsx @@ -13,7 +13,7 @@ type MissingPrivilegesDescriptionProps = Partial; export const MissingPrivilegesDescription = React.memo( ({ canCreateIntegrations, canCreateConnectors, canExecuteConnectors }) => { return ( - + {i18n.PRIVILEGES_REQUIRED_TITLE} diff --git a/x-pack/plugins/integration_assistant/public/common/components/success_section/success_section.tsx b/x-pack/plugins/integration_assistant/public/common/components/success_section/success_section.tsx index 62df4a8f98660..08da1329770cd 100644 --- a/x-pack/plugins/integration_assistant/public/common/components/success_section/success_section.tsx +++ b/x-pack/plugins/integration_assistant/public/common/components/success_section/success_section.tsx @@ -35,7 +35,13 @@ export const SuccessSection = React.memo(({ integrationName return ( - + (({ integrationName icon={} title={i18n.VIEW_INTEGRATION_TITLE} description={i18n.VIEW_INTEGRATION_DESCRIPTION} - footer={{i18n.VIEW_INTEGRATION_BUTTON}} + footer={ + + {i18n.VIEW_INTEGRATION_BUTTON} + + } /> diff --git a/x-pack/plugins/integration_assistant/public/components/create_integration/create_integration_assistant/steps/connector_step/connector_setup.tsx b/x-pack/plugins/integration_assistant/public/components/create_integration/create_integration_assistant/steps/connector_step/connector_setup.tsx index 8715f42eb8f58..e85481378f4dd 100644 --- a/x-pack/plugins/integration_assistant/public/components/create_integration/create_integration_assistant/steps/connector_step/connector_setup.tsx +++ b/x-pack/plugins/integration_assistant/public/components/create_integration/create_integration_assistant/steps/connector_step/connector_setup.tsx @@ -104,10 +104,13 @@ export const ConnectorSetup = React.memo( size="xl" color="text" type={actionTypeRegistry.get(actionType.id).iconClass} + data-test-subj="connectorActionId" /> - {actionType.name} + + {actionType.name} + diff --git a/x-pack/plugins/integration_assistant/public/components/create_integration/create_integration_landing/create_integration_landing.tsx b/x-pack/plugins/integration_assistant/public/components/create_integration/create_integration_landing/create_integration_landing.tsx index 39cbd2cea1026..71706625f636f 100644 --- a/x-pack/plugins/integration_assistant/public/components/create_integration/create_integration_landing/create_integration_landing.tsx +++ b/x-pack/plugins/integration_assistant/public/components/create_integration/create_integration_landing/create_integration_landing.tsx @@ -54,7 +54,10 @@ export const CreateIntegrationLanding = React.memo(() => { defaultMessage="If you have an existing integration package, {link}" values={{ link: ( - navigate(Page.upload)}> + navigate(Page.upload)} + data-test-subj="uploadPackageLink" + > { tooltipContent={i18n.TECH_PREVIEW_TOOLTIP} size="s" color="hollow" + data-test-subj="techPreviewBadge" /> @@ -64,7 +65,9 @@ export const IntegrationAssistantCard = React.memo(() => { {canExecuteConnectors ? ( - navigate(Page.assistant)}>{i18n.ASSISTANT_BUTTON} + navigate(Page.assistant)} data-test-subj="assistantButton"> + {i18n.ASSISTANT_BUTTON} + ) : ( {i18n.ASSISTANT_BUTTON} diff --git a/x-pack/plugins/integration_assistant/server/graphs/categorization/graph.ts b/x-pack/plugins/integration_assistant/server/graphs/categorization/graph.ts index 2517cc1c31886..227bcd6939b94 100644 --- a/x-pack/plugins/integration_assistant/server/graphs/categorization/graph.ts +++ b/x-pack/plugins/integration_assistant/server/graphs/categorization/graph.ts @@ -243,6 +243,6 @@ export async function getCategorizationGraph({ client, model }: CategorizationGr } ); - const compiledCategorizationGraph = workflow.compile(); + const compiledCategorizationGraph = workflow.compile().withConfig({ runName: 'Categorization' }); return compiledCategorizationGraph; } diff --git a/x-pack/plugins/integration_assistant/server/graphs/ecs/graph.ts b/x-pack/plugins/integration_assistant/server/graphs/ecs/graph.ts index efe2ac1aeb437..89a7e5c600723 100644 --- a/x-pack/plugins/integration_assistant/server/graphs/ecs/graph.ts +++ b/x-pack/plugins/integration_assistant/server/graphs/ecs/graph.ts @@ -78,8 +78,7 @@ export async function getEcsSubGraph({ model }: EcsGraphParams) { }) .addEdge('modelSubOutput', END); - const compiledEcsSubGraph = workflow.compile(); - + const compiledEcsSubGraph = workflow.compile().withConfig({ runName: 'ECS Mapping (Chunk)' }); return compiledEcsSubGraph; } @@ -120,7 +119,6 @@ export async function getEcsGraph({ model }: EcsGraphParams) { }) .addEdge('modelOutput', END); - const compiledEcsGraph = workflow.compile(); - + const compiledEcsGraph = workflow.compile().withConfig({ runName: 'ECS Mapping' }); return compiledEcsGraph; } diff --git a/x-pack/plugins/integration_assistant/server/graphs/ecs/validate.test.ts b/x-pack/plugins/integration_assistant/server/graphs/ecs/validate.test.ts index a7fb5962b5558..39c4e3ac4bab3 100644 --- a/x-pack/plugins/integration_assistant/server/graphs/ecs/validate.test.ts +++ b/x-pack/plugins/integration_assistant/server/graphs/ecs/validate.test.ts @@ -8,14 +8,14 @@ import { ECS_RESERVED } from './constants'; import { + extractECSMapping, findDuplicateFields, findInvalidEcsFields, - processMapping, removeReservedFields, } from './validate'; describe('Testing ecs handler', () => { - it('processMapping()', async () => { + it('extractECSMapping()', async () => { const path: string[] = []; const value = { checkpoint: { @@ -50,7 +50,7 @@ describe('Testing ecs handler', () => { }, }; const output: Record = {}; - await processMapping(path, value, output); + await extractECSMapping(path, value, output); expect(output).toEqual({ 'source.address': [['checkpoint', 'firewall', 'origin']], 'user.name': [['checkpoint', 'firewall', 'administrator']], @@ -96,6 +96,110 @@ describe('findInvalidEcsFields', () => { const invalid = findInvalidEcsFields(ecsMappingReserved); expect(invalid.length).toBe(1); }); + + it('invalid: date_format fields (natural example)', async () => { + const misspelledDateFormatMapping = { + ai_postgres_202410050058: { + logs: { + column1: { + target: 'event.created', + confidence: 0.9, + type: 'date', + date_format: ['yyyy-MM-dd HH:mm:ss.SSS z'], + }, + column12: { + target: 'log.level', + confidence: 0.95, + type: 'string', + date_format: [], + }, + column11: null, + column4: null, + column9: { + target: 'event.start', + confidence: 0.8, + type: 'date', + date_format: ['yyyy-MM-dd HH:mm:ss z'], + }, + column7: null, + column6: null, + column14: { + target: 'event.reason', + confidence: 0.7, + type: 'string', + date_format: [], + }, + column13: null, + column24: { + target: 'process.name', + confidence: 0.85, + type: 'string', + date_format: [], + }, + column23: null, + column10: null, + column5: { + target: 'source.address', + confidence: 0.9, + type: 'string', + date_format: [], + }, + column3: { + target: 'user.name', + confidence: 0.8, + type: 'string', + date_format: [], + }, + column2: { + target: 'destination.user.name', + confidence: 0.7, + type: 'string', + date_format: [], + }, + column8: null, + }, + }, + }; + + const invalid = findInvalidEcsFields(misspelledDateFormatMapping); + expect(invalid.length).toBe(1); + }); + + it('invalid: date_format fields (handcrafted example)', async () => { + const mixedMapping = { + some_title: { + logs: { + column1: { + target: 'event.created', + confidence: 0.9, + type: 'date', + date_format: ['yyyy-MM-dd HH:mm:ss.SSS z'], + }, + column12: { + target: 'log.level', + confidence: 0.95, + type: 'string', + date_formats: [], + }, + column11: null, + column4: null, + column9: { + target: 'event.start', + confidence: 0.8, + type: 'date', + date_format: 'yyyy-MM-dd HH:mm:ss z', + }, + column2: { + target: 'destination.user.name', + type: 'string', + date_format: [], + }, + }, + }, + }; + const invalid = findInvalidEcsFields(mixedMapping); + expect(invalid.length).toBe(1); + }); }); describe('findDuplicateFields', () => { diff --git a/x-pack/plugins/integration_assistant/server/graphs/ecs/validate.ts b/x-pack/plugins/integration_assistant/server/graphs/ecs/validate.ts index 62f88f2d234f2..033c6651982ea 100644 --- a/x-pack/plugins/integration_assistant/server/graphs/ecs/validate.ts +++ b/x-pack/plugins/integration_assistant/server/graphs/ecs/validate.ts @@ -10,7 +10,6 @@ import { mergeSamples } from '../../util/samples'; import { ECS_RESERVED } from './constants'; import type { EcsBaseNodeParams } from './types'; -const valueFieldKeys = new Set(['target', 'confidence', 'date_formats', 'type']); type AnyObject = Record; function extractKeys(data: AnyObject, prefix: string = ''): Set { @@ -46,43 +45,97 @@ function findMissingFields(combinedSamples: string, ecsMapping: AnyObject): stri return missingKeys; } -export function processMapping( +// Describes an LLM-generated ECS mapping candidate. +interface ECSFieldTarget { + target: string; + type: string; + confidence: number; + date_formats: string[]; +} + +/** + * Parses a given object as an ECSFieldTarget object if it meets the required structure. + * + * @param value - The value to be converted to an ECSMapping object. It should be an object + * with properties `target` and `type`. It should have `confidence` field and + * either `date_formats` or `date_format`, though we also fill in these otherwise. + * @returns An ECSFieldTarget object if the conversion succeeded, otherwise null. + */ +function asECSFieldTarget(value: any): ECSFieldTarget | null { + if (value === null || typeof value !== 'object' || Array.isArray(value)) { + return null; + } + + if ( + value.target && + typeof value.target === 'string' && + value.type && + typeof value.type === 'string' + ) { + let confidence = 0.5; + if (value.confidence && typeof value.confidence === 'number') { + confidence = value.confidence; + } + let dateFormats: string[] = []; + if (value.date_formats && Array.isArray(value.date_formats)) { + dateFormats = value.date_formats; + } else if (value.date_format && Array.isArray(value.date_format)) { + dateFormats = value.date_format; + } else if (value.date_format && typeof value.date_format === 'string') { + dateFormats = [value.date_format]; + } + return { + target: value.target, + type: value.type, + confidence, + date_formats: dateFormats, + }; + } + + return null; +} + +/** + * Extracts ECS (Elastic Common Schema) field mapping dictionary from the LLM output. + * + * @param path - The current path in the object being traversed (an array of strings). + * @param value - The value to be processed, which can be an array, object, or other types. + * @param output - A record where the extracted ECS mappings will be stored. The keys are ECS targets, and the values are arrays of paths. + * + * This function recursively traverses the provided value. If the value is an array, it processes each item in the array. + * If the value can be interpreted as an ECS mapping, it adds the path to the output record under the appropriate ECS target. + * If the value is a regular object, it continues traversing its properties. + */ +export function extractECSMapping( path: string[], value: any, output: Record ): void { - if (typeof value === 'object' && value !== null) { - if (!Array.isArray(value)) { - // If the value is a dict with all the keys returned for each source field, this is the full path of the field. - const valueKeys = new Set(Object.keys(value)); - - if ([...valueFieldKeys].every((k) => valueKeys.has(k))) { - if (value?.target !== null) { - if (!output[value?.target]) { - output[value.target] = []; - } - output[value.target].push(path); - } - } else { - // Regular dictionary, continue traversing - for (const [k, v] of Object.entries(value)) { - processMapping([...path, k], v, output); - } - } - } else { - // If the value is an array, iterate through items and process them - for (const item of value) { - if (typeof item === 'object' && item !== null) { - processMapping(path, item, output); - } + if (Array.isArray(value)) { + // If the value is an array, iterate through items and process them. + for (const item of value) { + if (typeof item === 'object' && item !== null) { + extractECSMapping(path, item, output); } } - } else if (value !== null) { - // Direct value, accumulate path - if (!output[value]) { - output[value] = []; + return; + } + + const ecsFieldTarget = asECSFieldTarget(value); + if (ecsFieldTarget) { + // If we can interpret the value as an ECSFieldTarget. + if (!output[ecsFieldTarget.target]) { + output[ecsFieldTarget.target] = []; + } + output[ecsFieldTarget.target].push(path); + return; + } + + if (typeof value === 'object' && value !== null) { + // Regular dictionary, continue traversing. + for (const [k, v] of Object.entries(value)) { + extractECSMapping([...path, k], v, output); } - output[value].push(path); } } @@ -96,7 +149,7 @@ export function findDuplicateFields(prefixedSamples: string[], ecsMapping: AnyOb const output: Record = {}; // Get all keys for each target ECS mapping field - processMapping([], ecsMapping, output); + extractECSMapping([], ecsMapping, output); // Filter out any ECS field that does not have multiple source fields mapped to it const filteredOutput = Object.fromEntries( @@ -138,7 +191,7 @@ export function findInvalidEcsFields(currentMapping: AnyObject): string[] { const ecsDict = ECS_FULL; const ecsReserved = ECS_RESERVED; - processMapping([], currentMapping, output); + extractECSMapping([], currentMapping, output); const filteredOutput = Object.fromEntries( Object.entries(output).filter(([key, _]) => key !== null) ); diff --git a/x-pack/plugins/integration_assistant/server/graphs/kv/graph.ts b/x-pack/plugins/integration_assistant/server/graphs/kv/graph.ts index 6f7b43ba40f22..f72984655c1f8 100644 --- a/x-pack/plugins/integration_assistant/server/graphs/kv/graph.ts +++ b/x-pack/plugins/integration_assistant/server/graphs/kv/graph.ts @@ -139,6 +139,6 @@ export async function getKVGraph({ model, client }: KVGraphParams) { }) .addEdge('modelOutput', END); - const compiledKVGraph = workflow.compile(); + const compiledKVGraph = workflow.compile().withConfig({ runName: 'Key-Value' }); return compiledKVGraph; } diff --git a/x-pack/plugins/integration_assistant/server/graphs/log_type_detection/graph.ts b/x-pack/plugins/integration_assistant/server/graphs/log_type_detection/graph.ts index b1cdecd39fe69..4a3f2e2536266 100644 --- a/x-pack/plugins/integration_assistant/server/graphs/log_type_detection/graph.ts +++ b/x-pack/plugins/integration_assistant/server/graphs/log_type_detection/graph.ts @@ -128,6 +128,6 @@ export async function getLogFormatDetectionGraph({ model, client }: LogDetection } ); - const compiledLogFormatDetectionGraph = workflow.compile(); + const compiledLogFormatDetectionGraph = workflow.compile().withConfig({ runName: 'Log Format' }); return compiledLogFormatDetectionGraph; } diff --git a/x-pack/plugins/integration_assistant/server/graphs/related/graph.ts b/x-pack/plugins/integration_assistant/server/graphs/related/graph.ts index 4ab623788c84e..be4b00852485c 100644 --- a/x-pack/plugins/integration_assistant/server/graphs/related/graph.ts +++ b/x-pack/plugins/integration_assistant/server/graphs/related/graph.ts @@ -207,6 +207,6 @@ export async function getRelatedGraph({ client, model }: RelatedGraphParams) { } ); - const compiledRelatedGraph = workflow.compile(); + const compiledRelatedGraph = workflow.compile().withConfig({ runName: 'Related' }); return compiledRelatedGraph; } diff --git a/x-pack/plugins/integration_assistant/server/graphs/unstructured/graph.ts b/x-pack/plugins/integration_assistant/server/graphs/unstructured/graph.ts index 6048404728bfb..cf3a645effa68 100644 --- a/x-pack/plugins/integration_assistant/server/graphs/unstructured/graph.ts +++ b/x-pack/plugins/integration_assistant/server/graphs/unstructured/graph.ts @@ -107,6 +107,6 @@ export async function getUnstructuredGraph({ model, client }: UnstructuredGraphP .addEdge('handleUnstructuredError', 'handleUnstructuredValidate') .addEdge('modelOutput', END); - const compiledUnstructuredGraph = workflow.compile(); + const compiledUnstructuredGraph = workflow.compile().withConfig({ runName: 'Unstructured' }); return compiledUnstructuredGraph; } diff --git a/x-pack/plugins/integration_assistant/server/integration_builder/readme_files.ts b/x-pack/plugins/integration_assistant/server/integration_builder/readme_files.ts index 163b2b04b52f9..5467a1549cea2 100644 --- a/x-pack/plugins/integration_assistant/server/integration_builder/readme_files.ts +++ b/x-pack/plugins/integration_assistant/server/integration_builder/readme_files.ts @@ -5,7 +5,7 @@ * 2.0. */ -import nunjucks from 'nunjucks'; +import { Environment, FileSystemLoader } from 'nunjucks'; import { join as joinPath } from 'path'; import { createSync, ensureDirSync } from '../util'; @@ -17,6 +17,8 @@ export function createReadme(packageDir: string, integrationName: string, fields function createPackageReadme(packageDir: string, integrationName: string, fields: object[]) { const dirPath = joinPath(packageDir, 'docs/'); + // The readme nunjucks template files should be named in the format `somename_readme.md.njk` and not just `readme.md.njk` + // since any file with `readme.*` pattern is skipped in build process in buildkite. createReadmeFile(dirPath, 'package_readme.md.njk', integrationName, fields); } @@ -33,10 +35,17 @@ function createReadmeFile( ) { ensureDirSync(targetDir); - const template = nunjucks.render(templateName, { + const templatesPath = joinPath(__dirname, '../templates'); + const env = new Environment(new FileSystemLoader(templatesPath), { + autoescape: false, + }); + + const template = env.getTemplate(templateName); + + const renderedTemplate = template.render({ package_name: integrationName, fields, }); - createSync(joinPath(targetDir, 'README.md'), template); + createSync(joinPath(targetDir, 'README.md'), renderedTemplate); } diff --git a/x-pack/plugins/integration_assistant/server/templates/build_readme.md.njk b/x-pack/plugins/integration_assistant/server/templates/build_readme.md.njk index e23fa4af9efe8..1b58e55aebd37 100644 --- a/x-pack/plugins/integration_assistant/server/templates/build_readme.md.njk +++ b/x-pack/plugins/integration_assistant/server/templates/build_readme.md.njk @@ -1,4 +1,4 @@ -{% include "readme.njk" %} +{% include "./description_readme.njk" %} {% for data_stream in fields %} ### {{ data_stream.datastream }} diff --git a/x-pack/plugins/integration_assistant/server/templates/readme.njk b/x-pack/plugins/integration_assistant/server/templates/description_readme.njk similarity index 100% rename from x-pack/plugins/integration_assistant/server/templates/readme.njk rename to x-pack/plugins/integration_assistant/server/templates/description_readme.njk diff --git a/x-pack/plugins/integration_assistant/server/templates/package_readme.md.njk b/x-pack/plugins/integration_assistant/server/templates/package_readme.md.njk index b47e3491b5bc2..bd56aba5ac1e5 100644 --- a/x-pack/plugins/integration_assistant/server/templates/package_readme.md.njk +++ b/x-pack/plugins/integration_assistant/server/templates/package_readme.md.njk @@ -1,4 +1,4 @@ -{% include "readme.njk" %} +{% include "./description_readme.njk" %} {% for data_stream in fields %} ### {{ data_stream.datastream }} diff --git a/x-pack/plugins/lens/common/expressions/datatable/utils.ts b/x-pack/plugins/lens/common/expressions/datatable/utils.ts index 71c3d92126b33..bc617d931f500 100644 --- a/x-pack/plugins/lens/common/expressions/datatable/utils.ts +++ b/x-pack/plugins/lens/common/expressions/datatable/utils.ts @@ -5,14 +5,37 @@ * 2.0. */ -import type { Datatable } from '@kbn/expressions-plugin/common'; +import { type Datatable, type DatatableColumnMeta } from '@kbn/expressions-plugin/common'; import { getOriginalId } from './transpose_helpers'; +/** + * Returns true for numerical fields + * + * Excludes the following types: + * - `range` - Stringified range + * - `multi_terms` - Multiple values + * - `filters` - Arbitrary label + * - `filtered_metric` - Array of values + */ +export function isNumericField(meta?: DatatableColumnMeta): boolean { + return ( + meta?.type === 'number' && + meta.params?.id !== 'range' && + meta.params?.id !== 'multi_terms' && + meta.sourceParams?.type !== 'filters' && + meta.sourceParams?.type !== 'filtered_metric' + ); +} + +/** + * Returns true for numerical fields, excluding ranges + */ export function isNumericFieldForDatatable(table: Datatable | undefined, accessor: string) { - return getFieldTypeFromDatatable(table, accessor) === 'number'; + const meta = getFieldMetaFromDatatable(table, accessor); + return isNumericField(meta); } -export function getFieldTypeFromDatatable(table: Datatable | undefined, accessor: string) { +export function getFieldMetaFromDatatable(table: Datatable | undefined, accessor: string) { return table?.columns.find((col) => col.id === accessor || getOriginalId(col.id) === accessor) - ?.meta.type; + ?.meta; } diff --git a/x-pack/plugins/lens/public/app_plugin/shared/edit_on_the_fly/lens_configuration_flyout.tsx b/x-pack/plugins/lens/public/app_plugin/shared/edit_on_the_fly/lens_configuration_flyout.tsx index ecc392a7e56b7..fd0407513f869 100644 --- a/x-pack/plugins/lens/public/app_plugin/shared/edit_on_the_fly/lens_configuration_flyout.tsx +++ b/x-pack/plugins/lens/public/app_plugin/shared/edit_on_the_fly/lens_configuration_flyout.tsx @@ -337,6 +337,7 @@ export function LensEditConfigurationFlyout({ setErrors([]); updateSuggestion?.(attrs); } + prevQuery.current = q; setIsVisualizationLoading(false); }, [ @@ -481,7 +482,6 @@ export function LensEditConfigurationFlyout({ query={query} onTextLangQueryChange={(q) => { setQuery(q); - prevQuery.current = q; }} detectedTimestamp={adHocDataViews?.[0]?.timeFieldName} hideTimeFilterInfo={hideTimeFilterInfo} @@ -497,7 +497,8 @@ export function LensEditConfigurationFlyout({ editorIsInline hideRunQueryText onTextLangQuerySubmit={async (q, a) => { - if (q) { + // do not run the suggestions if the query is the same as the previous one + if (q && !isEqual(q, prevQuery.current)) { setIsVisualizationLoading(true); await runQuery(q, a); } diff --git a/x-pack/plugins/lens/public/shared_components/coloring/utils.test.ts b/x-pack/plugins/lens/public/shared_components/coloring/utils.test.ts index 5a126565c251f..cc6044fc0f624 100644 --- a/x-pack/plugins/lens/public/shared_components/coloring/utils.test.ts +++ b/x-pack/plugins/lens/public/shared_components/coloring/utils.test.ts @@ -110,7 +110,7 @@ describe('findMinMaxByColumnId', () => { { a: 'shoes', b: 53 }, ], }) - ).toEqual({ b: { min: 2, max: 53 } }); + ).toEqual(new Map([['b', { min: 2, max: 53 }]])); }); }); diff --git a/x-pack/plugins/lens/public/shared_components/coloring/utils.ts b/x-pack/plugins/lens/public/shared_components/coloring/utils.ts index 211628a096189..c58fec1ddb03e 100644 --- a/x-pack/plugins/lens/public/shared_components/coloring/utils.ts +++ b/x-pack/plugins/lens/public/shared_components/coloring/utils.ts @@ -95,12 +95,12 @@ export const findMinMaxByColumnId = ( table: Datatable | undefined, getOriginalId: (id: string) => string = (id: string) => id ) => { - const minMax: Record = {}; + const minMaxMap = new Map(); if (table != null) { for (const columnId of columnIds) { const originalId = getOriginalId(columnId); - minMax[originalId] = minMax[originalId] || { + const minMax = minMaxMap.get(originalId) ?? { max: Number.NEGATIVE_INFINITY, min: Number.POSITIVE_INFINITY, }; @@ -108,19 +108,22 @@ export const findMinMaxByColumnId = ( const rowValue = row[columnId]; const numericValue = getNumericValue(rowValue); if (numericValue != null) { - if (minMax[originalId].min > numericValue) { - minMax[originalId].min = numericValue; + if (minMax.min > numericValue) { + minMax.min = numericValue; } - if (minMax[originalId].max < numericValue) { - minMax[originalId].max = numericValue; + if (minMax.max < numericValue) { + minMax.max = numericValue; } } }); + // what happens when there's no data in the table? Fallback to a percent range - if (minMax[originalId].max === Number.NEGATIVE_INFINITY) { - minMax[originalId] = getFallbackDataBounds(); + if (minMax.max === Number.NEGATIVE_INFINITY) { + minMaxMap.set(originalId, getFallbackDataBounds()); + } else { + minMaxMap.set(originalId, minMax); } } } - return minMax; + return minMaxMap; }; diff --git a/x-pack/plugins/lens/public/visualizations/datatable/components/cell_value.test.tsx b/x-pack/plugins/lens/public/visualizations/datatable/components/cell_value.test.tsx index 76b8fc7b61740..e9f3caba9ec05 100644 --- a/x-pack/plugins/lens/public/visualizations/datatable/components/cell_value.test.tsx +++ b/x-pack/plugins/lens/public/visualizations/datatable/components/cell_value.test.tsx @@ -54,9 +54,7 @@ describe('datatable cell renderer', () => { @@ -217,7 +215,7 @@ describe('datatable cell renderer', () => { { wrapper: DataContextProviderWrapper({ table, - minMaxByColumnId: { a: { min: 12, max: 155 } }, + minMaxByColumnId: new Map([['a', { min: 12, max: 155 }]]), ...context, }), } diff --git a/x-pack/plugins/lens/public/visualizations/datatable/components/cell_value.tsx b/x-pack/plugins/lens/public/visualizations/datatable/components/cell_value.tsx index 0761c7904e75f..97e7e755ac36e 100644 --- a/x-pack/plugins/lens/public/visualizations/datatable/components/cell_value.tsx +++ b/x-pack/plugins/lens/public/visualizations/datatable/components/cell_value.tsx @@ -53,7 +53,7 @@ export const createGridCell = ( } = columnConfig.columns[colIndex] ?? {}; const filterOnClick = oneClickFilter && handleFilterClick; const content = formatters[columnId]?.convert(rawRowValue, filterOnClick ? 'text' : 'html'); - const currentAlignment = alignments && alignments[columnId]; + const currentAlignment = alignments?.get(columnId); useEffect(() => { let colorSet = false; diff --git a/x-pack/plugins/lens/public/visualizations/datatable/components/columns.test.tsx b/x-pack/plugins/lens/public/visualizations/datatable/components/columns.test.tsx index 3612317f7a565..76437743c5723 100644 --- a/x-pack/plugins/lens/public/visualizations/datatable/components/columns.test.tsx +++ b/x-pack/plugins/lens/public/visualizations/datatable/components/columns.test.tsx @@ -72,7 +72,7 @@ const callCreateGridColumns = ( params.formatFactory ?? (((x: unknown) => ({ convert: () => x })) as unknown as FormatFactory), params.onColumnResize ?? jest.fn(), params.onColumnHide ?? jest.fn(), - params.alignments ?? {}, + params.alignments ?? new Map(), params.headerRowHeight ?? RowHeightMode.auto, params.headerRowLines ?? 1, params.columnCellValueActions ?? [], diff --git a/x-pack/plugins/lens/public/visualizations/datatable/components/columns.tsx b/x-pack/plugins/lens/public/visualizations/datatable/components/columns.tsx index 6cd8c32db4b6d..8d2fcc9fac0c0 100644 --- a/x-pack/plugins/lens/public/visualizations/datatable/components/columns.tsx +++ b/x-pack/plugins/lens/public/visualizations/datatable/components/columns.tsx @@ -51,7 +51,7 @@ export const createGridColumns = ( formatFactory: FormatFactory, onColumnResize: (eventData: { columnId: string; width: number | undefined }) => void, onColumnHide: ((eventData: { columnId: string }) => void) | undefined, - alignments: Record, + alignments: Map, headerRowHeight: RowHeightMode, headerRowLines: number, columnCellValueActions: LensCellValueAction[][] | undefined, @@ -261,7 +261,7 @@ export const createGridColumns = ( }); } } - const currentAlignment = alignments && alignments[field]; + const currentAlignment = alignments && alignments.get(field); const hasMultipleRows = [RowHeightMode.auto, RowHeightMode.custom, undefined].includes( headerRowHeight ); diff --git a/x-pack/plugins/lens/public/visualizations/datatable/components/dimension_editor.test.tsx b/x-pack/plugins/lens/public/visualizations/datatable/components/dimension_editor.test.tsx index 09c7d95b309e7..738f7edab2a6e 100644 --- a/x-pack/plugins/lens/public/visualizations/datatable/components/dimension_editor.test.tsx +++ b/x-pack/plugins/lens/public/visualizations/datatable/components/dimension_editor.test.tsx @@ -6,25 +6,20 @@ */ import React from 'react'; -import { DEFAULT_COLOR_MAPPING_CONFIG, type PaletteRegistry } from '@kbn/coloring'; +import { DEFAULT_COLOR_MAPPING_CONFIG } from '@kbn/coloring'; import { act, render, screen } from '@testing-library/react'; import userEvent, { type UserEvent } from '@testing-library/user-event'; import { chartPluginMock } from '@kbn/charts-plugin/public/mocks'; import { LayerTypes } from '@kbn/expression-xy-plugin/public'; import { EuiButtonGroupTestHarness } from '@kbn/test-eui-helpers'; -import { - FramePublicAPI, - OperationDescriptor, - VisualizationDimensionEditorProps, - DatasourcePublicAPI, - DataType, -} from '../../../types'; +import { FramePublicAPI, DatasourcePublicAPI, OperationDescriptor } from '../../../types'; import { DatatableVisualizationState } from '../visualization'; import { createMockDatasource, createMockFramePublicAPI } from '../../../mocks'; -import { TableDimensionEditor } from './dimension_editor'; +import { TableDimensionEditor, TableDimensionEditorProps } from './dimension_editor'; import { ColumnState } from '../../../../common/expressions'; import { capitalize } from 'lodash'; import { I18nProvider } from '@kbn/i18n-react'; +import { DatatableColumnType } from '@kbn/expressions-plugin/common'; describe('data table dimension editor', () => { let user: UserEvent; @@ -35,10 +30,8 @@ describe('data table dimension editor', () => { alignment: EuiButtonGroupTestHarness; }; let mockOperationForFirstColumn: (overrides?: Partial) => void; - let props: VisualizationDimensionEditorProps & { - paletteService: PaletteRegistry; - isDarkMode: boolean; - }; + + let props: TableDimensionEditorProps; function testState(): DatatableVisualizationState { return { @@ -80,6 +73,7 @@ describe('data table dimension editor', () => { name: 'foo', meta: { type: 'string', + params: {}, }, }, ], @@ -114,13 +108,7 @@ describe('data table dimension editor', () => { mockOperationForFirstColumn(); }); - const renderTableDimensionEditor = ( - overrideProps?: Partial< - VisualizationDimensionEditorProps & { - paletteService: PaletteRegistry; - } - > - ) => { + const renderTableDimensionEditor = (overrideProps?: Partial) => { return render(, { wrapper: ({ children }) => ( @@ -137,11 +125,18 @@ describe('data table dimension editor', () => { }); it('should render default alignment for number', () => { - mockOperationForFirstColumn({ dataType: 'number' }); + frame.activeData!.first.columns[0].meta.type = 'number'; renderTableDimensionEditor(); expect(btnGroups.alignment.selected).toHaveTextContent('Right'); }); + it('should render default alignment for ranges', () => { + frame.activeData!.first.columns[0].meta.type = 'number'; + frame.activeData!.first.columns[0].meta.params = { id: 'range' }; + renderTableDimensionEditor(); + expect(btnGroups.alignment.selected).toHaveTextContent('Left'); + }); + it('should render specific alignment', () => { state.columns[0].alignment = 'center'; renderTableDimensionEditor(); @@ -181,10 +176,11 @@ describe('data table dimension editor', () => { expect(screen.queryByTestId('lns_dynamicColoring_edit')).not.toBeInTheDocument(); }); - it.each(['date'])( + it.each(['date'])( 'should not show the dynamic coloring option for "%s" columns', - (dataType) => { - mockOperationForFirstColumn({ dataType }); + (type) => { + frame.activeData!.first.columns[0].meta.type = type; + renderTableDimensionEditor(); expect(screen.queryByTestId('lnsDatatable_dynamicColoring_groups')).not.toBeInTheDocument(); expect(screen.queryByTestId('lns_dynamicColoring_edit')).not.toBeInTheDocument(); @@ -231,15 +227,16 @@ describe('data table dimension editor', () => { }); }); - it.each<{ flyout: 'terms' | 'values'; isBucketed: boolean; dataType: DataType }>([ - { flyout: 'terms', isBucketed: true, dataType: 'number' }, - { flyout: 'terms', isBucketed: false, dataType: 'string' }, - { flyout: 'values', isBucketed: false, dataType: 'number' }, + it.each<{ flyout: 'terms' | 'values'; isBucketed: boolean; type: DatatableColumnType }>([ + { flyout: 'terms', isBucketed: true, type: 'number' }, + { flyout: 'terms', isBucketed: false, type: 'string' }, + { flyout: 'values', isBucketed: false, type: 'number' }, ])( - 'should show color by $flyout flyout when bucketing is $isBucketed with $dataType column', - async ({ flyout, isBucketed, dataType }) => { + 'should show color by $flyout flyout when bucketing is $isBucketed with $type column', + async ({ flyout, isBucketed, type }) => { state.columns[0].colorMode = 'cell'; - mockOperationForFirstColumn({ isBucketed, dataType }); + frame.activeData!.first.columns[0].meta.type = type; + mockOperationForFirstColumn({ isBucketed }); renderTableDimensionEditor(); await user.click(screen.getByLabelText('Edit colors')); @@ -251,6 +248,7 @@ describe('data table dimension editor', () => { it('should show the dynamic coloring option for a bucketed operation', () => { state.columns[0].colorMode = 'cell'; + frame.activeData!.first.columns[0].meta.type = 'string'; mockOperationForFirstColumn({ isBucketed: true }); renderTableDimensionEditor(); diff --git a/x-pack/plugins/lens/public/visualizations/datatable/components/dimension_editor.tsx b/x-pack/plugins/lens/public/visualizations/datatable/components/dimension_editor.tsx index c1e097276cf3d..99fe3cc1c164e 100644 --- a/x-pack/plugins/lens/public/visualizations/datatable/components/dimension_editor.tsx +++ b/x-pack/plugins/lens/public/visualizations/datatable/components/dimension_editor.tsx @@ -8,7 +8,7 @@ import React, { useCallback } from 'react'; import { i18n } from '@kbn/i18n'; import { EuiFormRow, EuiSwitch, EuiButtonGroup, htmlIdGenerator } from '@elastic/eui'; -import { PaletteRegistry } from '@kbn/coloring'; +import { PaletteRegistry, getFallbackDataBounds } from '@kbn/coloring'; import { getColorCategories } from '@kbn/chart-expressions-common'; import { useDebouncedValue } from '@kbn/visualization-utils'; import type { VisualizationDimensionEditorProps } from '../../../types'; @@ -26,6 +26,11 @@ import './dimension_editor.scss'; import { CollapseSetting } from '../../../shared_components/collapse_setting'; import { ColorMappingByValues } from '../../../shared_components/coloring/color_mapping_by_values'; import { ColorMappingByTerms } from '../../../shared_components/coloring/color_mapping_by_terms'; +import { getColumnAlignment } from '../utils'; +import { + getFieldMetaFromDatatable, + isNumericField, +} from '../../../../common/expressions/datatable/utils'; const idPrefix = htmlIdGenerator()(); @@ -45,12 +50,13 @@ function updateColumn( }); } -export function TableDimensionEditor( - props: VisualizationDimensionEditorProps & { +export type TableDimensionEditorProps = + VisualizationDimensionEditorProps & { paletteService: PaletteRegistry; isDarkMode: boolean; - } -) { + }; + +export function TableDimensionEditor(props: TableDimensionEditorProps) { const { frame, accessor, isInlineEditing, isDarkMode } = props; const column = props.state.columns.find(({ columnId }) => accessor === columnId); const { inputValue: localState, handleInputChange: setLocalState } = @@ -74,12 +80,13 @@ export function TableDimensionEditor( const currentData = frame.activeData?.[localState.layerId]; const datasource = frame.datasourceLayers?.[localState.layerId]; - const { dataType, isBucketed } = datasource?.getOperationForColumnId(accessor) ?? {}; - const showColorByTerms = shouldColorByTerms(dataType, isBucketed); - const currentAlignment = column?.alignment || (dataType === 'number' ? 'right' : 'left'); + const { isBucketed } = datasource?.getOperationForColumnId(accessor) ?? {}; + const meta = getFieldMetaFromDatatable(currentData, accessor); + const showColorByTerms = shouldColorByTerms(meta?.type, isBucketed); + const currentAlignment = getColumnAlignment(column, isNumericField(meta)); const currentColorMode = column?.colorMode || 'none'; const hasDynamicColoring = currentColorMode !== 'none'; - const showDynamicColoringFeature = dataType !== 'date'; + const showDynamicColoringFeature = meta?.type !== 'date'; const visibleColumnsCount = localState.columns.filter((c) => !c.hidden).length; const hasTransposedColumn = localState.columns.some(({ isTransposed }) => isTransposed); @@ -88,7 +95,7 @@ export function TableDimensionEditor( [] : [accessor]; const minMaxByColumnId = findMinMaxByColumnId(columnsToCheck, currentData, getOriginalId); - const currentMinMax = minMaxByColumnId[accessor]; + const currentMinMax = minMaxByColumnId.get(accessor) ?? getFallbackDataBounds(); const activePalette = column?.palette ?? { type: 'palette', diff --git a/x-pack/plugins/lens/public/visualizations/datatable/components/table_basic.test.tsx b/x-pack/plugins/lens/public/visualizations/datatable/components/table_basic.test.tsx index 21361f874e83e..2358b9ec5b563 100644 --- a/x-pack/plugins/lens/public/visualizations/datatable/components/table_basic.test.tsx +++ b/x-pack/plugins/lens/public/visualizations/datatable/components/table_basic.test.tsx @@ -11,7 +11,6 @@ import userEvent from '@testing-library/user-event'; import { I18nProvider } from '@kbn/i18n-react'; import faker from 'faker'; import { act } from 'react-dom/test-utils'; -import { IAggType } from '@kbn/data-plugin/public'; import { IFieldFormat } from '@kbn/field-formats-plugin/common'; import { coreMock } from '@kbn/core/public/mocks'; import { chartPluginMock } from '@kbn/charts-plugin/public/mocks'; @@ -73,6 +72,17 @@ function sampleArgs() { sourceParams: { indexPatternId, type: 'count' }, }, }, + { + id: 'd', + name: 'd', + meta: { + type: 'number', + source: 'esaggs', + field: 'd', + params: { id: 'range' }, + sourceParams: { indexPatternId, type: 'range' }, + }, + }, ], rows: [{ a: 'shoes', b: 1588024800000, c: 3 }], }; @@ -119,7 +129,9 @@ describe('DatatableComponent', () => { args, formatFactory: () => ({ convert: (x) => x } as IFieldFormat), dispatchEvent: onDispatchEvent, - getType: jest.fn(() => ({ type: 'buckets' } as IAggType)), + getType: jest.fn().mockReturnValue({ + type: 'buckets', + }), paletteService: chartPluginMock.createPaletteRegistry(), theme: setUpMockTheme, renderMode: 'edit' as const, @@ -357,14 +369,39 @@ describe('DatatableComponent', () => { ]); }); - test('it adds alignment data to context', () => { + test('it adds explicit alignment to context', () => { renderDatatableComponent({ args: { ...args, columns: [ { columnId: 'a', alignment: 'center', type: 'lens_datatable_column', colorMode: 'none' }, + { columnId: 'b', alignment: 'center', type: 'lens_datatable_column', colorMode: 'none' }, + { columnId: 'c', alignment: 'center', type: 'lens_datatable_column', colorMode: 'none' }, + { columnId: 'd', alignment: 'center', type: 'lens_datatable_column', colorMode: 'none' }, + ], + }, + }); + const alignmentsClassNames = screen + .getAllByTestId('lnsTableCellContent') + .map((cell) => cell.className); + + expect(alignmentsClassNames).toEqual([ + 'lnsTableCell--center', // set via args + 'lnsTableCell--center', // set via args + 'lnsTableCell--center', // set via args + 'lnsTableCell--center', // set via args + ]); + }); + + test('it adds default alignment data to context', () => { + renderDatatableComponent({ + args: { + ...args, + columns: [ + { columnId: 'a', type: 'lens_datatable_column', colorMode: 'none' }, { columnId: 'b', type: 'lens_datatable_column', colorMode: 'none' }, { columnId: 'c', type: 'lens_datatable_column', colorMode: 'none' }, + { columnId: 'd', type: 'lens_datatable_column', colorMode: 'none' }, ], sortingColumnId: 'b', sortingDirection: 'desc', @@ -375,9 +412,10 @@ describe('DatatableComponent', () => { .map((cell) => cell.className); expect(alignmentsClassNames).toEqual([ - 'lnsTableCell--center', // set via args + 'lnsTableCell--left', // default for string 'lnsTableCell--left', // default for date 'lnsTableCell--right', // default for number + 'lnsTableCell--left', // default for range ]); }); diff --git a/x-pack/plugins/lens/public/visualizations/datatable/components/table_basic.tsx b/x-pack/plugins/lens/public/visualizations/datatable/components/table_basic.tsx index 83249f86ffa79..55e198b943e81 100644 --- a/x-pack/plugins/lens/public/visualizations/datatable/components/table_basic.tsx +++ b/x-pack/plugins/lens/public/visualizations/datatable/components/table_basic.tsx @@ -6,7 +6,7 @@ */ import './table_basic.scss'; -import { ColorMappingInputData, PaletteOutput } from '@kbn/coloring'; +import { ColorMappingInputData, PaletteOutput, getFallbackDataBounds } from '@kbn/coloring'; import React, { useLayoutEffect, useCallback, @@ -58,8 +58,12 @@ import { } from './table_actions'; import { getFinalSummaryConfiguration } from '../../../../common/expressions/datatable/summary'; import { DEFAULT_HEADER_ROW_HEIGHT, DEFAULT_HEADER_ROW_HEIGHT_LINES } from './constants'; -import { getFieldTypeFromDatatable } from '../../../../common/expressions/datatable/utils'; +import { + getFieldMetaFromDatatable, + isNumericField, +} from '../../../../common/expressions/datatable/utils'; import { CellColorFn, getCellColorFn } from '../../../shared_components/coloring/get_cell_color_fn'; +import { getColumnAlignment } from '../utils'; export const DataContext = React.createContext({}); @@ -229,10 +233,7 @@ export const DatatableComponent = (props: DatatableRenderProps) => { columnConfig.columns .filter((_col, index) => { const col = firstTableRef.current.columns[index]; - return ( - col?.meta?.sourceParams?.type && - getType(col.meta.sourceParams.type as string)?.type === 'buckets' - ); + return getType(col?.meta)?.type === 'buckets'; }) .map((col) => col.columnId), [firstTableRef, columnConfig, getType] @@ -240,7 +241,7 @@ export const DatatableComponent = (props: DatatableRenderProps) => { const isEmpty = firstLocalTable.rows.length === 0 || - (bucketedColumns.length && + (bucketedColumns.length > 0 && props.data.rows.every((row) => bucketedColumns.every((col) => row[col] == null))); const visibleColumns = useMemo( @@ -266,34 +267,26 @@ export const DatatableComponent = (props: DatatableRenderProps) => { [onEditAction, setColumnConfig, columnConfig, isInteractive] ); - const isNumericMap: Record = useMemo( + const isNumericMap: Map = useMemo( () => - firstLocalTable.columns.reduce>( - (map, column) => ({ - ...map, - [column.id]: column.meta.type === 'number', - }), - {} - ), - [firstLocalTable] + firstLocalTable.columns.reduce((acc, column) => { + acc.set(column.id, isNumericField(column.meta)); + return acc; + }, new Map()), + [firstLocalTable.columns] ); - const alignments: Record = useMemo(() => { - const alignmentMap: Record = {}; - columnConfig.columns.forEach((column) => { - if (column.alignment) { - alignmentMap[column.columnId] = column.alignment; - } else { - alignmentMap[column.columnId] = isNumericMap[column.columnId] ? 'right' : 'left'; - } - }); - return alignmentMap; - }, [columnConfig, isNumericMap]); + const alignments: Map = useMemo(() => { + return columnConfig.columns.reduce((acc, column) => { + acc.set(column.columnId, getColumnAlignment(column, isNumericMap.get(column.columnId))); + return acc; + }, new Map()); + }, [columnConfig.columns, isNumericMap]); - const minMaxByColumnId: Record = useMemo(() => { + const minMaxByColumnId: Map = useMemo(() => { return findMinMaxByColumnId( columnConfig.columns - .filter(({ columnId }) => isNumericMap[columnId]) + .filter(({ columnId }) => isNumericMap.get(columnId)) .map(({ columnId }) => columnId), props.data, getOriginalId @@ -402,7 +395,7 @@ export const DatatableComponent = (props: DatatableRenderProps) => { return cellColorFnMap.get(originalId)!; } - const dataType = getFieldTypeFromDatatable(firstLocalTable, originalId); + const dataType = getFieldMetaFromDatatable(firstLocalTable, originalId)?.type; const isBucketed = bucketedColumns.some((id) => id === columnId); const colorByTerms = shouldColorByTerms(dataType, isBucketed); @@ -419,7 +412,7 @@ export const DatatableComponent = (props: DatatableRenderProps) => { : { type: 'ranges', bins: 0, - ...minMaxByColumnId[originalId], + ...(minMaxByColumnId.get(originalId) ?? getFallbackDataBounds()), }; const colorFn = getCellColorFn( props.paletteService, @@ -491,7 +484,7 @@ export const DatatableComponent = (props: DatatableRenderProps) => { ]) ); return ({ columnId }: { columnId: string }) => { - const currentAlignment = alignments && alignments[columnId]; + const currentAlignment = alignments.get(columnId); const alignmentClassName = `lnsTableCell--${currentAlignment}`; const columnName = columns.find(({ id }) => id === columnId)?.displayAsText?.replace(/ /g, '-') || columnId; diff --git a/x-pack/plugins/lens/public/visualizations/datatable/components/types.ts b/x-pack/plugins/lens/public/visualizations/datatable/components/types.ts index b884a2c716be9..00d916bf956ae 100644 --- a/x-pack/plugins/lens/public/visualizations/datatable/components/types.ts +++ b/x-pack/plugins/lens/public/visualizations/datatable/components/types.ts @@ -8,7 +8,7 @@ import { CoreSetup } from '@kbn/core/public'; import type { PaletteRegistry } from '@kbn/coloring'; import type { IAggType } from '@kbn/data-plugin/public'; -import type { Datatable, RenderMode } from '@kbn/expressions-plugin/common'; +import type { Datatable, DatatableColumnMeta, RenderMode } from '@kbn/expressions-plugin/common'; import type { ILensInterpreterRenderHandlers, LensCellValueAction, @@ -49,7 +49,7 @@ export type LensPagesizeAction = LensEditEvent export type DatatableRenderProps = DatatableProps & { formatFactory: FormatFactory; dispatchEvent: ILensInterpreterRenderHandlers['event']; - getType: (name: string) => IAggType | undefined; + getType: (meta?: DatatableColumnMeta) => IAggType | undefined; renderMode: RenderMode; paletteService: PaletteRegistry; theme: CoreSetup['theme']; @@ -72,8 +72,8 @@ export type DatatableRenderProps = DatatableProps & { export interface DataContextType { table?: Datatable; rowHasRowClickTriggerActions?: boolean[]; - alignments?: Record; - minMaxByColumnId?: Record; + alignments?: Map; + minMaxByColumnId?: Map; handleFilterClick?: ( field: string, value: unknown, diff --git a/x-pack/plugins/lens/public/visualizations/datatable/expression.tsx b/x-pack/plugins/lens/public/visualizations/datatable/expression.tsx index 652abec75695e..a5927dd9183bf 100644 --- a/x-pack/plugins/lens/public/visualizations/datatable/expression.tsx +++ b/x-pack/plugins/lens/public/visualizations/datatable/expression.tsx @@ -13,6 +13,7 @@ import type { IAggType } from '@kbn/data-plugin/public'; import { CoreSetup, IUiSettingsClient } from '@kbn/core/public'; import type { Datatable, + DatatableColumnMeta, ExpressionRenderDefinition, IInterpreterRenderHandlers, } from '@kbn/expressions-plugin/common'; @@ -102,6 +103,11 @@ export const getDatatableRenderer = (dependencies: { handlers.onDestroy(() => ReactDOM.unmountComponentAtNode(domNode)); const resolvedGetType = await dependencies.getType; + const getType = (meta?: DatatableColumnMeta): IAggType | undefined => { + if (meta?.sourceParams?.type === undefined) return; + return resolvedGetType(String(meta.sourceParams.type)); + }; + const { hasCompatibleActions, isInteractive, getCompatibleCellValueActions } = handlers; const renderComplete = () => { @@ -161,7 +167,7 @@ export const getDatatableRenderer = (dependencies: { dispatchEvent={handlers.event} renderMode={handlers.getRenderMode()} paletteService={dependencies.paletteService} - getType={resolvedGetType} + getType={getType} rowHasRowClickTriggerActions={rowHasRowClickTriggerActions} columnCellValueActions={columnCellValueActions} columnFilterable={columnsFilterable} diff --git a/x-pack/plugins/lens/public/visualizations/datatable/index.ts b/x-pack/plugins/lens/public/visualizations/datatable/index.ts index f68f167ea5f02..93e5e38e03c3c 100644 --- a/x-pack/plugins/lens/public/visualizations/datatable/index.ts +++ b/x-pack/plugins/lens/public/visualizations/datatable/index.ts @@ -32,6 +32,7 @@ export class DatatableVisualization { '../../async_services' ); const palettes = await charts.palettes.getPalettes(); + expressions.registerRenderer(() => getDatatableRenderer({ formatFactory, @@ -44,7 +45,10 @@ export class DatatableVisualization { }) ); - return getDatatableVisualization({ paletteService: palettes, kibanaTheme: core.theme }); + return getDatatableVisualization({ + paletteService: palettes, + kibanaTheme: core.theme, + }); }); } } diff --git a/x-pack/plugins/lens/public/visualizations/datatable/utils.ts b/x-pack/plugins/lens/public/visualizations/datatable/utils.ts new file mode 100644 index 0000000000000..ab4d8f05f8d44 --- /dev/null +++ b/x-pack/plugins/lens/public/visualizations/datatable/utils.ts @@ -0,0 +1,14 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +export function getColumnAlignment( + { alignment }: C, + isNumeric = false +): 'left' | 'right' | 'center' { + if (alignment) return alignment; + return isNumeric ? 'right' : 'left'; +} diff --git a/x-pack/plugins/lens/public/visualizations/datatable/visualization.tsx b/x-pack/plugins/lens/public/visualizations/datatable/visualization.tsx index 0187776985a30..d2d23b2033f90 100644 --- a/x-pack/plugins/lens/public/visualizations/datatable/visualization.tsx +++ b/x-pack/plugins/lens/public/visualizations/datatable/visualization.tsx @@ -147,8 +147,8 @@ export const getDatatableVisualization = ({ .map(({ id }) => id) || [] : [accessor]; const minMaxByColumnId = findMinMaxByColumnId(columnsToCheck, currentData, getOriginalId); - - if (palette && !showColorByTerms && !palette?.canDynamicColoring) { + const dataBounds = minMaxByColumnId.get(accessor); + if (palette && !showColorByTerms && !palette?.canDynamicColoring && dataBounds) { const newPalette: PaletteOutput = { type: 'palette', name: showColorByTerms ? 'default' : defaultPaletteParams.name, @@ -158,7 +158,7 @@ export const getDatatableVisualization = ({ palette: { ...newPalette, params: { - stops: applyPaletteParams(paletteService, newPalette, minMaxByColumnId[accessor]), + stops: applyPaletteParams(paletteService, newPalette, dataBounds), }, }, }; diff --git a/x-pack/plugins/lens/public/visualizations/heatmap/utils.ts b/x-pack/plugins/lens/public/visualizations/heatmap/utils.ts index 5e09ce2987bae..fe942dd40427c 100644 --- a/x-pack/plugins/lens/public/visualizations/heatmap/utils.ts +++ b/x-pack/plugins/lens/public/visualizations/heatmap/utils.ts @@ -26,7 +26,10 @@ export function getSafePaletteParams( accessor, }; const minMaxByColumnId = findMinMaxByColumnId([accessor], currentData); - const currentMinMax = minMaxByColumnId[accessor]; + const currentMinMax = minMaxByColumnId.get(accessor) ?? { + max: Number.NEGATIVE_INFINITY, + min: Number.POSITIVE_INFINITY, + }; // need to tell the helper that the colorStops are required to display return { diff --git a/x-pack/plugins/ml/common/types/capabilities.ts b/x-pack/plugins/ml/common/types/capabilities.ts index 7dc841a5cb4d7..e7a097fae7dc4 100644 --- a/x-pack/plugins/ml/common/types/capabilities.ts +++ b/x-pack/plugins/ml/common/types/capabilities.ts @@ -58,6 +58,7 @@ export const adminMlCapabilities = { canResetJob: false, canUpdateJob: false, canForecastJob: false, + canDeleteForecast: false, canCreateDatafeed: false, canDeleteDatafeed: false, canStartStopDatafeed: false, @@ -222,6 +223,7 @@ export const featureCapabilities: FeatureCapabilities = { 'canResetJob', 'canUpdateJob', 'canForecastJob', + 'canDeleteForecast', 'canCreateDatafeed', 'canDeleteDatafeed', 'canStartStopDatafeed', diff --git a/x-pack/plugins/ml/public/application/capabilities/check_capabilities.ts b/x-pack/plugins/ml/public/application/capabilities/check_capabilities.ts index ed930b857473d..734993a4e4a6e 100644 --- a/x-pack/plugins/ml/public/application/capabilities/check_capabilities.ts +++ b/x-pack/plugins/ml/public/application/capabilities/check_capabilities.ts @@ -269,6 +269,10 @@ export function createPermissionFailureMessage(privilegeType: keyof MlCapabiliti message = i18n.translate('xpack.ml.privilege.noPermission.runForecastsTooltip', { defaultMessage: 'You do not have permission to run forecasts.', }); + } else if (privilegeType === 'canDeleteForecast') { + message = i18n.translate('xpack.ml.privilege.noPermission.deleteForecastsTooltip', { + defaultMessage: 'You do not have permission to delete forecasts.', + }); } return i18n.translate('xpack.ml.privilege.pleaseContactAdministratorTooltip', { defaultMessage: '{message} Please contact your administrator.', diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/analytics_list/use_table_settings.ts b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/analytics_list/use_table_settings.ts index 670efb1627ef7..fa24a65c425bc 100644 --- a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/analytics_list/use_table_settings.ts +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/analytics_list/use_table_settings.ts @@ -30,9 +30,11 @@ export interface CriteriaWithPagination extends Criteria { }; } -interface UseTableSettingsReturnValue { +interface UseTableSettingsReturnValue { onTableChange: EuiBasicTableProps['onChange']; - pagination: Required>; + pagination: HidePagination extends true + ? Required> | boolean + : Required>; sorting: { sort: { field: keyof T; @@ -44,8 +46,31 @@ interface UseTableSettingsReturnValue { export function useTableSettings( totalItemCount: number, pageState: ListingPageUrlState, - updatePageState: (update: Partial) => void -): UseTableSettingsReturnValue { + updatePageState: (update: Partial) => void, + hide: true +): UseTableSettingsReturnValue; + +export function useTableSettings( + totalItemCount: number, + pageState: ListingPageUrlState, + updatePageState: (update: Partial) => void, + hide?: false +): UseTableSettingsReturnValue; + +/** + * + * @param totalItemCount + * @param pageState + * @param updatePageState + * @param hide If true, hides pagination when total number of items is lower that the smallest per page option + * @returns + */ +export function useTableSettings( + totalItemCount: number, + pageState: ListingPageUrlState, + updatePageState: (update: Partial) => void, + hide: boolean = false +): UseTableSettingsReturnValue { const { pageIndex, pageSize, sortField, sortDirection } = pageState; const onTableChange: EuiBasicTableProps['onChange'] = useCallback( @@ -66,15 +91,19 @@ export function useTableSettings( [pageState, updatePageState] ); - const pagination = useMemo( - () => ({ + const pagination = useMemo(() => { + if (hide && totalItemCount <= Math.min(...PAGE_SIZE_OPTIONS)) { + // Hide pagination if total number of items is lower that the smallest per page option + return false; + } + + return { pageIndex, pageSize, totalItemCount, pageSizeOptions: PAGE_SIZE_OPTIONS, - }), - [totalItemCount, pageIndex, pageSize] - ); + }; + }, [totalItemCount, pageIndex, pageSize, hide]); const sorting = useMemo( () => ({ diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/source_selection/source_selection.tsx b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/source_selection/source_selection.tsx index ae03a0779edf1..5aa0ccc46a5cd 100644 --- a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/source_selection/source_selection.tsx +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/source_selection/source_selection.tsx @@ -11,7 +11,7 @@ import { EuiCallOut, EuiPageBody, EuiPanel, EuiSpacer } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import { getNestedProperty } from '@kbn/ml-nested-property'; import { SavedObjectFinder } from '@kbn/saved-objects-finder-plugin/public'; -import type { SavedObjectCommon } from '@kbn/saved-objects-finder-plugin/common'; +import type { FinderAttributes, SavedObjectCommon } from '@kbn/saved-objects-finder-plugin/common'; import { CreateDataViewButton } from '../../../../../components/create_data_view_button'; import { useMlKibana, useNavigateToPath } from '../../../../../contexts/kibana'; import { useToastNotificationService } from '../../../../../services/toast_notification_service'; @@ -22,6 +22,8 @@ import { const fixedPageSize: number = 20; +type SavedObject = SavedObjectCommon; + export const SourceSelection: FC = () => { const { services: { @@ -41,7 +43,7 @@ export const SourceSelection: FC = () => { id: string, type: string, fullName?: string, - savedObject?: SavedObjectCommon + savedObject?: SavedObject ) => { // Kibana data views including `:` are cross-cluster search indices // and are not supported by Data Frame Analytics yet. For saved searches @@ -142,6 +144,9 @@ export const SourceSelection: FC = () => { defaultMessage: 'Saved search', } ), + showSavedObject: (savedObject: SavedObject) => + // ES|QL Based saved searches are not supported in DFA, filter them out + savedObject.attributes.isTextBasedQuery !== true, }, { type: 'index-pattern', diff --git a/x-pack/plugins/ml/public/application/datavisualizer/data_drift/data_view_editor.tsx b/x-pack/plugins/ml/public/application/datavisualizer/data_drift/data_view_editor.tsx index 5f52ef1c928f8..eafe31cb0f355 100644 --- a/x-pack/plugins/ml/public/application/datavisualizer/data_drift/data_view_editor.tsx +++ b/x-pack/plugins/ml/public/application/datavisualizer/data_drift/data_view_editor.tsx @@ -80,8 +80,7 @@ export function DataViewEditor({ const { onTableChange, pagination } = useTableSettings( matchedReferenceIndices.length, pageState, - // @ts-expect-error callback will have all the 4 necessary params - updatePageState + updatePageState as Parameters['2'] ); const pageOfItems = useMemo(() => { diff --git a/x-pack/plugins/ml/public/application/datavisualizer/data_drift/index_patterns_picker.tsx b/x-pack/plugins/ml/public/application/datavisualizer/data_drift/index_patterns_picker.tsx index 04dd9ca764ab0..1f31ce934e442 100644 --- a/x-pack/plugins/ml/public/application/datavisualizer/data_drift/index_patterns_picker.tsx +++ b/x-pack/plugins/ml/public/application/datavisualizer/data_drift/index_patterns_picker.tsx @@ -14,12 +14,16 @@ import { FormattedMessage } from '@kbn/i18n-react'; import { SavedObjectFinder } from '@kbn/saved-objects-finder-plugin/public'; import { type DataViewEditorService as DataViewEditorServiceSpec } from '@kbn/data-view-editor-plugin/public'; import { INDEX_PATTERN_TYPE } from '@kbn/data-views-plugin/public'; +import type { FinderAttributes, SavedObjectCommon } from '@kbn/saved-objects-finder-plugin/common'; import { createPath } from '../../routing/router'; import { ML_PAGES } from '../../../../common/constants/locator'; import { DataDriftIndexPatternsEditor } from './data_drift_index_patterns_editor'; import { MlPageHeader } from '../../components/page_header'; import { useMlKibana, useNavigateToPath } from '../../contexts/kibana'; + +type SavedObject = SavedObjectCommon; + export const DataDriftIndexOrSearchRedirect: FC = () => { const navigateToPath = useNavigateToPath(); const { contentManagement, uiSettings } = useMlKibana().services; @@ -65,6 +69,9 @@ export const DataDriftIndexOrSearchRedirect: FC = () => { defaultMessage: 'Saved search', } ), + showSavedObject: (savedObject: SavedObject) => + // ES|QL Based saved searches are not supported in Data Drift, filter them out + savedObject.attributes.isTextBasedQuery !== true, }, { type: 'index-pattern', diff --git a/x-pack/plugins/ml/public/application/jobs/jobs_list/components/job_details/forecasts_table/forecasts_table.js b/x-pack/plugins/ml/public/application/jobs/jobs_list/components/job_details/forecasts_table/forecasts_table.js index 5f1cbb1c76ca0..bfed613b9ad5d 100644 --- a/x-pack/plugins/ml/public/application/jobs/jobs_list/components/job_details/forecasts_table/forecasts_table.js +++ b/x-pack/plugins/ml/public/application/jobs/jobs_list/components/job_details/forecasts_table/forecasts_table.js @@ -9,8 +9,8 @@ import PropTypes from 'prop-types'; import React, { Component } from 'react'; import { - EuiButtonIcon, EuiCallOut, + EuiConfirmModal, EuiFlexGroup, EuiFlexItem, EuiInMemoryTable, @@ -32,9 +32,43 @@ import { isTimeSeriesViewJob, } from '../../../../../../../common/util/job_utils'; import { ML_APP_LOCATOR, ML_PAGES } from '../../../../../../../common/constants/locator'; +import { checkPermission } from '../../../../../capabilities/check_capabilities'; const MAX_FORECASTS = 500; +const DeleteForecastConfirm = ({ onCancel, onConfirm }) => ( + +

    + +

    + +); + /** * Table component for rendering the lists of forecasts run on an ML job. */ @@ -44,6 +78,7 @@ export class ForecastsTable extends Component { this.state = { isLoading: props.job.data_counts.processed_record_count !== 0, forecasts: [], + forecastIdToDelete: undefined, }; this.mlForecastService = forecastServiceFactory(constructorContext.services.mlServices.mlApi); } @@ -54,6 +89,11 @@ export class ForecastsTable extends Component { static contextType = context; componentDidMount() { + this.loadForecasts(); + this.canDeleteJobForecast = checkPermission('canDeleteForecast'); + } + + async loadForecasts() { const dataCounts = this.props.job.data_counts; if (dataCounts.processed_record_count > 0) { // Get the list of all the forecasts with results at or later than the specified 'from' time. @@ -163,6 +203,36 @@ export class ForecastsTable extends Component { await navigateToUrl(singleMetricViewerForecastLink); } + async deleteForecast(forecastId) { + const { + services: { + mlServices: { mlApi }, + }, + } = this.context; + + this.setState({ + isLoading: true, + forecastIdToDelete: undefined, + }); + + try { + await mlApi.deleteForecast({ jobId: this.props.job.job_id, forecastId }); + } catch (error) { + this.setState({ + forecastIdToDelete: undefined, + isLoading: false, + errorMessage: i18n.translate( + 'xpack.ml.jobsList.jobDetails.forecastsTable.deleteForecastErrorMessage', + { + defaultMessage: 'An error occurred when deleting the forecast.', + } + ), + }); + } + + this.loadForecasts(); + } + render() { if (this.state.isLoading === true) { return ( @@ -302,48 +372,74 @@ export class ForecastsTable extends Component { textOnly: true, }, { - name: i18n.translate('xpack.ml.jobsList.jobDetails.forecastsTable.viewLabel', { - defaultMessage: 'View', + width: '75px', + name: i18n.translate('xpack.ml.jobsList.jobDetails.forecastsTable.actionsLabel', { + defaultMessage: 'Actions', }), - width: '60px', - render: (forecast) => { - const viewForecastAriaLabel = i18n.translate( - 'xpack.ml.jobsList.jobDetails.forecastsTable.viewAriaLabel', - { - defaultMessage: 'View forecast created at {createdDate}', - values: { - createdDate: timeFormatter(forecast.forecast_create_timestamp), - }, - } - ); - - return ( - this.openSingleMetricView(forecast)} - isDisabled={ + actions: [ + { + description: i18n.translate('xpack.ml.jobsList.jobDetails.forecastsTable.viewLabel', { + defaultMessage: 'View', + }), + type: 'icon', + icon: 'eye', + enabled: (forecast) => + !( this.props.job.blocked !== undefined || forecast.forecast_status !== FORECAST_REQUEST_STATE.FINISHED - } - iconType="singleMetricViewer" - aria-label={viewForecastAriaLabel} - data-test-subj="mlJobListForecastTabOpenSingleMetricViewButton" - /> - ); - }, + ), + onClick: (forecast) => this.openSingleMetricView(forecast), + 'data-test-subj': 'mlJobListForecastTabOpenSingleMetricViewButton', + }, + ...(this.canDeleteJobForecast + ? [ + { + description: i18n.translate( + 'xpack.ml.jobsList.jobDetails.forecastsTable.deleteForecastDescription', + { + defaultMessage: 'Delete forecast', + } + ), + type: 'icon', + icon: 'trash', + color: 'danger', + enabled: () => this.state.isLoading === false, + onClick: (item) => { + this.setState({ + forecastIdToDelete: item.forecast_id, + }); + }, + 'data-test-subj': 'mlJobListForecastTabDeleteForecastButton', + }, + ] + : []), + ], }, ]; return ( - + <> + + {this.state.forecastIdToDelete !== undefined ? ( + + this.setState({ + forecastIdToDelete: undefined, + }) + } + onConfirm={() => this.deleteForecast(this.state.forecastIdToDelete)} + /> + ) : null} + ); } } diff --git a/x-pack/plugins/ml/public/application/jobs/new_job/pages/index_or_search/page.tsx b/x-pack/plugins/ml/public/application/jobs/new_job/pages/index_or_search/page.tsx index 972b8dc09e3ef..6e630ca61886f 100644 --- a/x-pack/plugins/ml/public/application/jobs/new_job/pages/index_or_search/page.tsx +++ b/x-pack/plugins/ml/public/application/jobs/new_job/pages/index_or_search/page.tsx @@ -11,6 +11,7 @@ import { EuiFlexGroup, EuiPageBody, EuiPanel } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n-react'; import { SavedObjectFinder } from '@kbn/saved-objects-finder-plugin/public'; +import type { FinderAttributes, SavedObjectCommon } from '@kbn/saved-objects-finder-plugin/common'; import { CreateDataViewButton } from '../../../../components/create_data_view_button'; import { useMlKibana, useNavigateToPath } from '../../../../contexts/kibana'; import { MlPageHeader } from '../../../../components/page_header'; @@ -21,6 +22,8 @@ export interface PageProps { const RESULTS_PER_PAGE = 20; +type SavedObject = SavedObjectCommon; + export const Page: FC = ({ nextStepPath, extraButtons, @@ -69,6 +72,9 @@ export const Page: FC = ({ defaultMessage: 'Saved search', } ), + showSavedObject: (savedObject: SavedObject) => + // ES|QL Based saved searches are not supported across ML, filter them out + savedObject.attributes.isTextBasedQuery !== true, }, { type: 'index-pattern', diff --git a/x-pack/plugins/ml/public/application/model_management/delete_models_modal.tsx b/x-pack/plugins/ml/public/application/model_management/delete_models_modal.tsx index 1e08ae9874567..0f5c515c22776 100644 --- a/x-pack/plugins/ml/public/application/model_management/delete_models_modal.tsx +++ b/x-pack/plugins/ml/public/application/model_management/delete_models_modal.tsx @@ -35,7 +35,7 @@ interface DeleteModelsModalProps { export const DeleteModelsModal: FC = ({ models, onClose }) => { const trainedModelsApiService = useTrainedModelsApiService(); - const { displayErrorToast, displaySuccessToast } = useToastNotificationService(); + const { displayErrorToast } = useToastNotificationService(); const [canDeleteModel, setCanDeleteModel] = useState(false); const [deletePipelines, setDeletePipelines] = useState(false); @@ -66,16 +66,6 @@ export const DeleteModelsModal: FC = ({ models, onClose }) ) ); - displaySuccessToast( - i18n.translate('xpack.ml.trainedModels.modelsList.successfullyDeletedMessage', { - defaultMessage: - '{modelsCount, plural, one {Model {modelIds}} other {# models}} {modelsCount, plural, one {has} other {have}} been successfully deleted', - values: { - modelsCount: modelIds.length, - modelIds: modelIds.join(', '), - }, - }) - ); } catch (error) { displayErrorToast( error, diff --git a/x-pack/plugins/ml/public/application/model_management/get_model_state.tsx b/x-pack/plugins/ml/public/application/model_management/get_model_state.tsx index 8591a3b9e8dc9..d8bf2b8084a6a 100644 --- a/x-pack/plugins/ml/public/application/model_management/get_model_state.tsx +++ b/x-pack/plugins/ml/public/application/model_management/get_model_state.tsx @@ -5,8 +5,17 @@ * 2.0. */ +import React from 'react'; import { DEPLOYMENT_STATE, MODEL_STATE, type ModelState } from '@kbn/ml-trained-models-utils'; -import type { EuiHealthProps } from '@elastic/eui'; +import { + EuiBadge, + EuiHealth, + EuiLoadingSpinner, + type EuiHealthProps, + EuiFlexGroup, + EuiFlexItem, + EuiText, +} from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import type { ModelItem } from './models_list'; @@ -33,11 +42,11 @@ export const getModelDeploymentState = (model: ModelItem): ModelState | undefine export const getModelStateColor = ( state: ModelState | undefined -): { color: EuiHealthProps['color']; name: string } | null => { +): { color: EuiHealthProps['color']; name: string; component?: React.ReactNode } | null => { switch (state) { case MODEL_STATE.DOWNLOADED: return { - color: 'subdued', + color: 'success', name: i18n.translate('xpack.ml.trainedModels.modelsList.modelState.downloadedName', { defaultMessage: 'Ready to deploy', }), @@ -46,37 +55,64 @@ export const getModelStateColor = ( return { color: 'primary', name: i18n.translate('xpack.ml.trainedModels.modelsList.modelState.downloadingName', { - defaultMessage: 'Downloading...', + defaultMessage: 'Downloading', }), }; case MODEL_STATE.STARTED: return { - color: 'success', + color: '#E6F9F7', name: i18n.translate('xpack.ml.trainedModels.modelsList.modelState.startedName', { defaultMessage: 'Deployed', }), + get component() { + return ( + + + {this.name} + + + ); + }, }; case MODEL_STATE.STARTING: return { color: 'success', name: i18n.translate('xpack.ml.trainedModels.modelsList.modelState.startingName', { - defaultMessage: 'Starting deployment...', + defaultMessage: 'Deploying', }), + get component() { + return ( + + + + + + {this.name} + + + ); + }, }; case MODEL_STATE.STOPPING: return { color: 'accent', name: i18n.translate('xpack.ml.trainedModels.modelsList.modelState.stoppingName', { - defaultMessage: 'Stopping deployment...', + defaultMessage: 'Stopping', }), + get component() { + return ( + + + + + + {this.name} + + + ); + }, }; case MODEL_STATE.NOT_DOWNLOADED: - return { - color: '#d4dae5', - name: i18n.translate('xpack.ml.trainedModels.modelsList.modelState.notDownloadedName', { - defaultMessage: 'Not downloaded', - }), - }; default: return null; } diff --git a/x-pack/plugins/ml/public/application/model_management/model_actions.tsx b/x-pack/plugins/ml/public/application/model_management/model_actions.tsx index b9e0c39578349..b4ddff093933a 100644 --- a/x-pack/plugins/ml/public/application/model_management/model_actions.tsx +++ b/x-pack/plugins/ml/public/application/model_management/model_actions.tsx @@ -8,7 +8,7 @@ import type { Action } from '@elastic/eui/src/components/basic_table/action_types'; import { i18n } from '@kbn/i18n'; import { isPopulatedObject } from '@kbn/ml-is-populated-object'; -import { EuiToolTip } from '@elastic/eui'; +import { EuiToolTip, useIsWithinMaxBreakpoint } from '@elastic/eui'; import React, { useCallback, useMemo, useEffect, useState } from 'react'; import { BUILT_IN_MODEL_TAG, @@ -53,6 +53,8 @@ export function useModelActions({ fetchModels: () => Promise; modelAndDeploymentIds: string[]; }): Array> { + const isMobileLayout = useIsWithinMaxBreakpoint('l'); + const { services: { application: { navigateToUrl }, @@ -132,7 +134,7 @@ export function useModelActions({ [] ); - return useMemo( + return useMemo>>( () => [ { name: i18n.translate('xpack.ml.trainedModels.modelsList.viewTrainingDataNameActionLabel', { @@ -203,12 +205,18 @@ export function useModelActions({ ), 'data-test-subj': 'mlModelsTableRowStartDeploymentAction', icon: 'play', - type: 'icon', + // @ts-ignore + type: isMobileLayout ? 'icon' : 'button', isPrimary: true, + color: 'success', enabled: (item) => { - return canStartStopTrainedModels && !isLoading && item.state !== MODEL_STATE.DOWNLOADING; + return canStartStopTrainedModels && !isLoading; + }, + available: (item) => { + return ( + item.model_type === TRAINED_MODEL_TYPE.PYTORCH && item.state === MODEL_STATE.DOWNLOADED + ); }, - available: (item) => item.model_type === TRAINED_MODEL_TYPE.PYTORCH, onClick: async (item) => { const modelDeploymentParams = await getUserInputModelDeploymentParams( item, @@ -234,14 +242,6 @@ export function useModelActions({ : {}), } ); - displaySuccessToast( - i18n.translate('xpack.ml.trainedModels.modelsList.startSuccess', { - defaultMessage: 'Deployment for "{modelId}" has been started successfully.', - values: { - modelId: item.model_id, - }, - }) - ); await fetchModels(); } catch (e) { displayErrorToast( @@ -342,6 +342,7 @@ export function useModelActions({ available: (item) => item.model_type === TRAINED_MODEL_TYPE.PYTORCH && canStartStopTrainedModels && + // Deployment can be either started, starting, or exist in a failed state (item.state === MODEL_STATE.STARTED || item.state === MODEL_STATE.STARTING) && // Only show the action if there is at least one deployment that is not used by the inference service (!Array.isArray(item.inference_apis) || @@ -373,16 +374,6 @@ export function useModelActions({ force: requireForceStop, } ); - displaySuccessToast( - i18n.translate('xpack.ml.trainedModels.modelsList.stopSuccess', { - defaultMessage: - '{numberOfDeployments, plural, one {Deployment} other {Deployments}} for "{modelId}" has been stopped successfully.', - values: { - modelId: item.model_id, - numberOfDeployments: deploymentIds.length, - }, - }) - ); if (Object.values(results).some((r) => r.error !== undefined)) { Object.entries(results).forEach(([id, r]) => { if (r.error !== undefined) { @@ -423,7 +414,9 @@ export function useModelActions({ }), 'data-test-subj': 'mlModelsTableRowDownloadModelAction', icon: 'download', - type: 'icon', + color: 'text', + // @ts-ignore + type: isMobileLayout ? 'icon' : 'button', isPrimary: true, available: (item) => canCreateTrainedModels && item.state === MODEL_STATE.NOT_DOWNLOADED, enabled: (item) => !isLoading, @@ -480,10 +473,16 @@ export function useModelActions({ }, { name: (model) => { - return ( + return model.state === MODEL_STATE.DOWNLOADING ? ( + <> + {i18n.translate('xpack.ml.trainedModels.modelsList.deleteModelActionLabel', { + defaultMessage: 'Cancel', + })} + + ) : ( <> {i18n.translate('xpack.ml.trainedModels.modelsList.deleteModelActionLabel', { - defaultMessage: 'Delete model', + defaultMessage: 'Delete', })} ); @@ -491,27 +490,35 @@ export function useModelActions({ description: (model: ModelItem) => { const hasDeployments = model.deployment_ids.length > 0; const { hasInferenceServices } = model; - return hasInferenceServices - ? i18n.translate( - 'xpack.ml.trainedModels.modelsList.deleteDisabledWithInferenceServicesTooltip', - { - defaultMessage: 'Model is used by the _inference API', - } - ) - : hasDeployments - ? i18n.translate( - 'xpack.ml.trainedModels.modelsList.deleteDisabledWithDeploymentsTooltip', - { - defaultMessage: 'Model has started deployments', - } - ) - : i18n.translate('xpack.ml.trainedModels.modelsList.deleteModelActionLabel', { - defaultMessage: 'Delete model', - }); + + if (model.state === MODEL_STATE.DOWNLOADING) { + return i18n.translate('xpack.ml.trainedModels.modelsList.cancelDownloadActionLabel', { + defaultMessage: 'Cancel download', + }); + } else if (hasInferenceServices) { + return i18n.translate( + 'xpack.ml.trainedModels.modelsList.deleteDisabledWithInferenceServicesTooltip', + { + defaultMessage: 'Model is used by the _inference API', + } + ); + } else if (hasDeployments) { + return i18n.translate( + 'xpack.ml.trainedModels.modelsList.deleteDisabledWithDeploymentsTooltip', + { + defaultMessage: 'Model has started deployments', + } + ); + } else { + return i18n.translate('xpack.ml.trainedModels.modelsList.deleteModelActionLabel', { + defaultMessage: 'Delete model', + }); + } }, 'data-test-subj': 'mlModelsTableRowDeleteAction', icon: 'trash', - type: 'icon', + // @ts-ignore + type: isMobileLayout ? 'icon' : 'button', color: 'danger', isPrimary: false, onClick: (model) => { @@ -539,9 +546,10 @@ export function useModelActions({ }), 'data-test-subj': 'mlModelsTableRowTestAction', icon: 'inputOutput', - type: 'icon', + // @ts-ignore + type: isMobileLayout ? 'icon' : 'button', isPrimary: true, - available: isTestable, + available: (item) => isTestable(item, true), onClick: (item) => { if (isDfaTrainedModel(item) && !isBuiltInModel(item)) { onDfaTestAction(item); @@ -550,7 +558,7 @@ export function useModelActions({ } }, enabled: (item) => { - return canTestTrainedModels && isTestable(item, true) && !isLoading; + return canTestTrainedModels && !isLoading; }, }, { @@ -612,6 +620,7 @@ export function useModelActions({ trainedModelsApiService, urlLocator, onModelDownloadRequest, + isMobileLayout, ] ); } diff --git a/x-pack/plugins/ml/public/application/model_management/models_list.tsx b/x-pack/plugins/ml/public/application/model_management/models_list.tsx index 3aef951f1545e..f218030c65ad3 100644 --- a/x-pack/plugins/ml/public/application/model_management/models_list.tsx +++ b/x-pack/plugins/ml/public/application/model_management/models_list.tsx @@ -5,9 +5,6 @@ * 2.0. */ -import type { FC } from 'react'; -import { useRef } from 'react'; -import React, { useCallback, useEffect, useMemo, useState } from 'react'; import type { SearchFilterConfig } from '@elastic/eui'; import { EuiBadge, @@ -16,52 +13,43 @@ import { EuiCallOut, EuiFlexGroup, EuiFlexItem, - EuiHealth, EuiIcon, EuiInMemoryTable, EuiLink, EuiProgress, EuiSpacer, EuiSwitch, + EuiText, EuiTitle, EuiToolTip, type EuiSearchBarProps, } from '@elastic/eui'; -import { groupBy, isEmpty } from 'lodash'; -import { i18n } from '@kbn/i18n'; -import { FormattedMessage } from '@kbn/i18n-react'; import type { EuiBasicTableColumn } from '@elastic/eui/src/components/basic_table/basic_table'; import type { EuiTableSelectionType } from '@elastic/eui/src/components/basic_table/table_types'; -import { FIELD_FORMAT_IDS } from '@kbn/field-formats-plugin/common'; -import { isPopulatedObject } from '@kbn/ml-is-populated-object'; -import { usePageUrlState } from '@kbn/ml-url-state'; +import { i18n } from '@kbn/i18n'; +import { FormattedMessage } from '@kbn/i18n-react'; import { useTimefilter } from '@kbn/ml-date-picker'; -import type { DeploymentState } from '@kbn/ml-trained-models-utils'; +import { isDefined } from '@kbn/ml-is-defined'; +import { isPopulatedObject } from '@kbn/ml-is-populated-object'; +import { useStorage } from '@kbn/ml-local-storage'; import { BUILT_IN_MODEL_TAG, BUILT_IN_MODEL_TYPE, - DEPLOYMENT_STATE, - ELASTIC_MODEL_DEFINITIONS, ELASTIC_MODEL_TAG, ELASTIC_MODEL_TYPE, ELSER_ID_V1, MODEL_STATE, type ModelState, } from '@kbn/ml-trained-models-utils'; -import { isDefined } from '@kbn/ml-is-defined'; -import { useStorage } from '@kbn/ml-local-storage'; +import type { ListingPageUrlState } from '@kbn/ml-url-state'; +import { usePageUrlState } from '@kbn/ml-url-state'; import { dynamic } from '@kbn/shared-ux-utility'; +import { cloneDeep, groupBy, isEmpty, memoize } from 'lodash'; +import type { FC } from 'react'; +import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react'; import useMountedState from 'react-use/lib/useMountedState'; -import type { ListingPageUrlState } from '@kbn/ml-url-state'; -import { getModelStateColor, getModelDeploymentState } from './get_model_state'; +import { ML_PAGES } from '../../../common/constants/locator'; import { ML_ELSER_CALLOUT_DISMISSED } from '../../../common/types/storage'; -import { TechnicalPreviewBadge } from '../components/technical_preview_badge'; -import { useModelActions } from './model_actions'; -import { ModelsTableToConfigMapping } from './config_mapping'; -import type { ModelsBarStats } from '../components/stats_bar'; -import { StatsBar } from '../components/stats_bar'; -import { useMlKibana } from '../contexts/kibana'; -import { useTrainedModelsApiService } from '../services/ml_api_service/trained_models'; import type { ModelDownloadState, ModelPipelines, @@ -69,17 +57,23 @@ import type { TrainedModelDeploymentStatsResponse, TrainedModelStat, } from '../../../common/types/trained_models'; -import { DeleteModelsModal } from './delete_models_modal'; -import { ML_PAGES } from '../../../common/constants/locator'; +import { AddInferencePipelineFlyout } from '../components/ml_inference'; +import { SavedObjectsWarning } from '../components/saved_objects_warning'; +import type { ModelsBarStats } from '../components/stats_bar'; +import { StatsBar } from '../components/stats_bar'; +import { TechnicalPreviewBadge } from '../components/technical_preview_badge'; +import { useMlKibana } from '../contexts/kibana'; +import { useEnabledFeatures } from '../contexts/ml'; import { useTableSettings } from '../data_frame_analytics/pages/analytics_management/components/analytics_list/use_table_settings'; -import { useToastNotificationService } from '../services/toast_notification_service'; -import { useFieldFormatter } from '../contexts/kibana/use_field_formatter'; import { useRefresh } from '../routing/use_refresh'; -import { SavedObjectsWarning } from '../components/saved_objects_warning'; -import { TestModelAndPipelineCreationFlyout } from './test_models'; +import { useTrainedModelsApiService } from '../services/ml_api_service/trained_models'; +import { useToastNotificationService } from '../services/toast_notification_service'; +import { ModelsTableToConfigMapping } from './config_mapping'; +import { DeleteModelsModal } from './delete_models_modal'; +import { getModelDeploymentState, getModelStateColor } from './get_model_state'; +import { useModelActions } from './model_actions'; import { TestDfaModelsFlyout } from './test_dfa_models_flyout'; -import { AddInferencePipelineFlyout } from '../components/ml_inference'; -import { useEnabledFeatures } from '../contexts/ml'; +import { TestModelAndPipelineCreationFlyout } from './test_models'; type Stats = Omit; @@ -106,6 +100,7 @@ export type ModelItem = TrainedModelConfigResponse & { softwareLicense?: string; licenseUrl?: string; downloadState?: ModelDownloadState; + disclaimer?: string; }; export type ModelItemFull = Required; @@ -165,8 +160,6 @@ export const ModelsList: FC = ({ useTimefilter({ timeRangeSelector: false, autoRefreshSelector: true }); - const dateFormatter = useFieldFormatter(FIELD_FORMAT_IDS.DATE); - // allow for an internally controlled page state which stores the state in the URL // or an external page state, which is passed in as a prop. // external page state is used on the management page. @@ -188,7 +181,7 @@ export const ModelsList: FC = ({ const trainedModelsApiService = useTrainedModelsApiService(); - const { displayErrorToast, displaySuccessToast } = useToastNotificationService(); + const { displayErrorToast } = useToastNotificationService(); const [isInitialized, setIsInitialized] = useState(false); const [isLoading, setIsLoading] = useState(false); @@ -219,28 +212,9 @@ export const ModelsList: FC = ({ }, [items]); /** - * Checks if the model download complete. + * Fetch of model definitions available for download needs to happen only once */ - const isDownloadComplete = useCallback( - async (modelId: string): Promise => { - try { - const response = await trainedModelsApiService.getTrainedModels(modelId, { - include: 'definition_status', - }); - // @ts-ignore - return !!response[0]?.fully_defined; - } catch (error) { - displayErrorToast( - error, - i18n.translate('xpack.ml.trainedModels.modelsList.downloadStatusCheckErrorMessage', { - defaultMessage: 'Failed to check download status', - }) - ); - } - return false; - }, - [trainedModelsApiService, displayErrorToast] - ); + const getTrainedModelDownloads = memoize(trainedModelsApiService.getTrainedModelDownloads); /** * Fetches trained models. @@ -288,15 +262,20 @@ export const ModelsList: FC = ({ const idMap = new Map( resultItems.map((model) => [model.model_id, model]) ); - const forDownload = await trainedModelsApiService.getTrainedModelDownloads(); + /** + * Fetches model definitions available for download + */ + const forDownload = await getTrainedModelDownloads(); + const notDownloaded: ModelItem[] = forDownload - .filter(({ model_id: modelId, hidden, recommended, supported }) => { + .filter(({ model_id: modelId, hidden, recommended, supported, disclaimer }) => { if (idMap.has(modelId)) { const model = idMap.get(modelId)!; if (recommended) { model.recommended = true; } model.supported = supported; + model.disclaimer = disclaimer; } return !idMap.has(modelId) && !hidden; }) @@ -315,23 +294,28 @@ export const ModelsList: FC = ({ softwareLicense: modelDefinition.license, licenseUrl: modelDefinition.licenseUrl, supported: modelDefinition.supported, + disclaimer: modelDefinition.disclaimer, } as ModelItem; }); resultItems = [...resultItems, ...notDownloaded]; } - setItems(resultItems); - - if (expandedItemsToRefresh.length > 0) { - await fetchModelsStats(expandedItemsToRefresh); - - setItemIdToExpandedRowMap( - expandedItemsToRefresh.reduce((acc, item) => { - acc[item.model_id] = ; - return acc; - }, {} as Record) - ); - } + setItems((prevItems) => { + // Need to merge existing items with new items + // to preserve state and download status + return resultItems.map((item) => { + const prevItem = prevItems.find((i) => i.model_id === item.model_id); + return { + ...item, + ...(prevItem?.state === MODEL_STATE.DOWNLOADING + ? { + state: prevItem.state, + downloadState: prevItem.downloadState, + } + : {}), + }; + }); + }); } catch (error) { displayErrorToast( error, @@ -340,7 +324,9 @@ export const ModelsList: FC = ({ }) ); } + setIsInitialized(true); + setIsLoading(false); await fetchDownloadStatus(); @@ -399,20 +385,6 @@ export const ModelsList: FC = ({ return c.reason ?? ''; }, ''); }); - - const elasticModels = models.filter((model) => - Object.hasOwn(ELASTIC_MODEL_DEFINITIONS, model.model_id) - ); - if (elasticModels.length > 0) { - for (const model of elasticModels) { - if (Object.values(DEPLOYMENT_STATE).includes(model.state as DeploymentState)) { - // no need to check for the download status if the model has been deployed - continue; - } - const isDownloaded = await isDownloadComplete(model.model_id); - model.state = isDownloaded ? MODEL_STATE.DOWNLOADED : MODEL_STATE.DOWNLOADING; - } - } } return true; @@ -429,6 +401,8 @@ export const ModelsList: FC = ({ }, []); const downLoadStatusFetchInProgress = useRef(false); + const abortedDownload = useRef(new Set()); + /** * Updates model list with download status */ @@ -448,47 +422,43 @@ export const ModelsList: FC = ({ if (isMounted()) { setItems((prevItems) => { return prevItems.map((item) => { - const newItem = { ...item }; + if (!item.type?.includes('pytorch')) { + return item; + } + const newItem = cloneDeep(item); + if (downloadStatus[item.model_id]) { + newItem.state = MODEL_STATE.DOWNLOADING; newItem.downloadState = downloadStatus[item.model_id]; } else { - if (downloadInProgress.has(item.model_id)) { + /* Unfortunately, model download status does not report 100% download state, only from 1 to 99. Hence, there might be 3 cases + * 1. Model is not downloaded at all + * 2. Model download was in progress and finished + * 3. Model download was in progress and aborted + */ + delete newItem.downloadState; + + if (abortedDownload.current.has(item.model_id)) { + // Change downloading state to not downloaded + newItem.state = MODEL_STATE.NOT_DOWNLOADED; + abortedDownload.current.delete(item.model_id); + } else if (downloadInProgress.has(item.model_id) || !newItem.state) { // Change downloading state to downloaded - delete newItem.downloadState; newItem.state = MODEL_STATE.DOWNLOADED; } + + downloadInProgress.delete(item.model_id); } return newItem; }); }); } - const downloadedModelIds = Array.from(downloadInProgress).filter( - (v) => !downloadStatus[v] - ); - - if (downloadedModelIds.length > 0) { - // Show success toast - displaySuccessToast( - i18n.translate('xpack.ml.trainedModels.modelsList.downloadCompleteSuccess', { - defaultMessage: - '"{modelIds}" {modelIdsLength, plural, one {has} other {have}} been downloaded successfully.', - values: { - modelIds: downloadedModelIds.join(', '), - modelIdsLength: downloadedModelIds.length, - }, - }) - ); - } - Object.keys(downloadStatus).forEach((modelId) => { if (downloadStatus[modelId]) { downloadInProgress.add(modelId); } }); - downloadedModelIds.forEach((v) => { - downloadInProgress.delete(v); - }); if (isEmpty(downloadStatus)) { downLoadStatusFetchInProgress.current = false; @@ -501,7 +471,7 @@ export const ModelsList: FC = ({ downLoadStatusFetchInProgress.current = false; } }, - [trainedModelsApiService, displaySuccessToast, isMounted] + [trainedModelsApiService, isMounted] ); /** @@ -575,7 +545,6 @@ export const ModelsList: FC = ({ if (itemIdToExpandedRowMapValues[item.model_id]) { delete itemIdToExpandedRowMapValues[item.model_id]; } else { - await fetchModelsStats([item]); itemIdToExpandedRowMapValues[item.model_id] = ; } setItemIdToExpandedRowMap(itemIdToExpandedRowMapValues); @@ -583,9 +552,8 @@ export const ModelsList: FC = ({ const columns: Array> = [ { - align: 'left', - width: '32px', isExpander: true, + align: 'center', render: (item: ModelItem) => { if (!item.stats) { return null; @@ -610,35 +578,25 @@ export const ModelsList: FC = ({ }, { name: modelIdColumnName, - width: '15%', sortable: ({ model_id: modelId }: ModelItem) => modelId, truncateText: false, textOnly: false, 'data-test-subj': 'mlModelsTableColumnId', - render: ({ description, model_id: modelId }: ModelItem) => { + render: ({ + description, + model_id: modelId, + recommended, + supported, + type, + disclaimer, + }: ModelItem) => { const isTechPreview = description?.includes('(Tech Preview)'); - return ( - - {modelId} - {isTechPreview ? ( - - - - ) : null} - - ); - }, - }, - { - name: i18n.translate('xpack.ml.trainedModels.modelsList.modelDescriptionHeader', { - defaultMessage: 'Description', - }), - truncateText: false, - 'data-test-subj': 'mlModelsTableColumnDescription', - render: ({ description, recommended, tags, supported }: ModelItem) => { - if (!description) return null; - const descriptionText = description.replace('(Tech Preview)', ''); + let descriptionText = description?.replace('(Tech Preview)', ''); + + if (disclaimer) { + descriptionText += '. ' + disclaimer; + } const tooltipContent = supported === false ? ( @@ -653,80 +611,98 @@ export const ModelsList: FC = ({ /> ) : null; - return tooltipContent ? ( - - <> - {descriptionText}  - - - - ) : ( - descriptionText + return ( + + + + {modelId} + + {isTechPreview ? ( + + + + ) : null} + + + {descriptionText ? ( + + {descriptionText} + {tooltipContent ? ( + <> +   + + + + + ) : null} + + ) : null} + + {Array.isArray(type) && type.length > 0 ? ( + + {type.map((t) => ( + + + + {t} + + + + ))} + + ) : null} + ); }, }, { - width: '15%', - field: ModelsTableToConfigMapping.type, - name: i18n.translate('xpack.ml.trainedModels.modelsList.typeHeader', { - defaultMessage: 'Type', - }), - sortable: true, - truncateText: true, - align: 'left', - render: (types: string[]) => ( - - {types.map((type) => ( - - - {type} - - - ))} - - ), - 'data-test-subj': 'mlModelsTableColumnType', - }, - { - width: '10%', name: i18n.translate('xpack.ml.trainedModels.modelsList.stateHeader', { defaultMessage: 'State', }), - align: 'left', truncateText: false, + width: '150px', render: ({ state, downloadState }: ModelItem) => { const config = getModelStateColor(state); if (!config) return null; - const isDownloadInProgress = state === MODEL_STATE.DOWNLOADING && downloadState; + const isProgressbarVisible = state === MODEL_STATE.DOWNLOADING && downloadState; const label = ( - + {config.name} - + ); return ( - {isDownloadInProgress ? ( + {isProgressbarVisible ? ( - {((downloadState.downloaded_parts / downloadState.total_parts) * 100).toFixed( - 0 - ) + '%'} + {downloadState + ? ( + (downloadState.downloaded_parts / downloadState.total_parts) * + 100 + ).toFixed(0) + '%' + : '100%'} } - value={downloadState?.downloaded_parts} - max={downloadState?.total_parts} + value={downloadState?.downloaded_parts ?? 1} + max={downloadState?.total_parts ?? 1} size="xs" color={config.color} /> ) : ( - {label} + + {config.component ?? label} + )} ); @@ -734,21 +710,10 @@ export const ModelsList: FC = ({ 'data-test-subj': 'mlModelsTableColumnDeploymentState', }, { - width: '20%', - field: ModelsTableToConfigMapping.createdAt, - name: i18n.translate('xpack.ml.trainedModels.modelsList.createdAtHeader', { - defaultMessage: 'Created at', - }), - dataType: 'date', - render: (v: number) => dateFormatter(v), - sortable: true, - 'data-test-subj': 'mlModelsTableColumnCreatedAt', - }, - { - width: '15%', name: i18n.translate('xpack.ml.trainedModels.modelsList.actionsHeader', { defaultMessage: 'Actions', }), + width: '200px', actions, 'data-test-subj': 'mlModelsTableColumnActions', }, @@ -836,7 +801,8 @@ export const ModelsList: FC = ({ const { onTableChange, pagination, sorting } = useTableSettings( items.length, pageState, - updatePageState + updatePageState, + true ); const search: EuiSearchBarProps = { @@ -921,6 +887,7 @@ export const ModelsList: FC = ({
    + tableLayout={'auto'} responsiveBreakpoint={'xl'} allowNeutralSort={false} columns={columns} @@ -974,7 +941,14 @@ export const ModelsList: FC = ({ {modelsToDelete.length > 0 && ( { + modelsToDelete.forEach((model) => { + if (model.state === MODEL_STATE.DOWNLOADING) { + abortedDownload.current.add(model.model_id); + } + }); + setModelsToDelete([]); + if (refreshList) { fetchModelsData(); } diff --git a/x-pack/plugins/ml/public/application/routing/routes/timeseriesexplorer/state_manager.tsx b/x-pack/plugins/ml/public/application/routing/routes/timeseriesexplorer/state_manager.tsx index fa1753a4342fc..309f24dd1c62b 100644 --- a/x-pack/plugins/ml/public/application/routing/routes/timeseriesexplorer/state_manager.tsx +++ b/x-pack/plugins/ml/public/application/routing/routes/timeseriesexplorer/state_manager.tsx @@ -216,6 +216,10 @@ export const TimeSeriesExplorerUrlStateManager: FC { + if (selectedForecastIdProp !== selectedForecastId) { + setSelectedForecastIdProp(undefined); + } + if ( autoZoomDuration !== undefined && boundsMinMs !== undefined && @@ -223,9 +227,6 @@ export const TimeSeriesExplorerUrlStateManager: FC { diff --git a/x-pack/plugins/ml/public/application/services/ml_api_service/index.ts b/x-pack/plugins/ml/public/application/services/ml_api_service/index.ts index fa6d179059eec..868ca0d5baa0f 100644 --- a/x-pack/plugins/ml/public/application/services/ml_api_service/index.ts +++ b/x-pack/plugins/ml/public/application/services/ml_api_service/index.ts @@ -103,6 +103,10 @@ export interface GetModelSnapshotsResponse { model_snapshots: ModelSnapshot[]; } +export interface DeleteForecastResponse { + acknowledged: boolean; +} + export function mlApiProvider(httpService: HttpService) { return { getJobs(obj?: { jobId?: string }) { @@ -368,6 +372,14 @@ export function mlApiProvider(httpService: HttpService) { }); }, + deleteForecast({ jobId, forecastId }: { jobId: string; forecastId: string }) { + return httpService.http({ + path: `${ML_INTERNAL_BASE_PATH}/anomaly_detectors/${jobId}/_forecast/${forecastId}`, + method: 'DELETE', + version: '1', + }); + }, + overallBuckets({ jobId, topN, diff --git a/x-pack/plugins/ml/public/application/timeseriesexplorer/components/forecasting_modal/forecasting_modal.js b/x-pack/plugins/ml/public/application/timeseriesexplorer/components/forecasting_modal/forecasting_modal.js index 5ac0dd68700d6..1bd47ff69ebc6 100644 --- a/x-pack/plugins/ml/public/application/timeseriesexplorer/components/forecasting_modal/forecasting_modal.js +++ b/x-pack/plugins/ml/public/application/timeseriesexplorer/components/forecasting_modal/forecasting_modal.js @@ -30,9 +30,13 @@ import { forecastServiceFactory } from '../../../services/forecast_service'; import { ForecastButton } from './forecast_button'; export const FORECAST_DURATION_MAX_DAYS = 3650; // Max forecast duration allowed by analytics. - -const FORECAST_JOB_MIN_VERSION = '6.1.0'; // Forecasting only allowed for jobs created >= 6.1.0. +const STATUS_FINISHED_QUERY = { + term: { + forecast_status: FORECAST_REQUEST_STATE.FINISHED, + }, +}; const FORECASTS_VIEW_MAX = 5; // Display links to a maximum of 5 forecasts. +const FORECAST_JOB_MIN_VERSION = '6.1.0'; // Forecasting only allowed for jobs created >= 6.1.0. const FORECAST_DURATION_MAX_MS = FORECAST_DURATION_MAX_DAYS * 86400000; const WARN_NUM_PARTITIONS = 100; // Warn about running a forecast with this number of field values. const FORECAST_STATS_POLL_FREQUENCY = 250; // Frequency in ms at which to poll for forecast request stats. @@ -64,6 +68,7 @@ export class ForecastingModal extends Component { latestRecordTimestamp: PropTypes.number, entities: PropTypes.array, setForecastId: PropTypes.func, + selectedForecastId: PropTypes.string, }; constructor(props) { @@ -405,13 +410,8 @@ export class ForecastingModal extends Component { // Get the list of all the finished forecasts for this job with results at or later than the dashboard 'from' time. const { timefilter } = this.context.services.data.query.timefilter; const bounds = timefilter.getActiveBounds(); - const statusFinishedQuery = { - term: { - forecast_status: FORECAST_REQUEST_STATE.FINISHED, - }, - }; this.mlForecastService - .getForecastsSummary(job, statusFinishedQuery, bounds.min.valueOf(), FORECASTS_VIEW_MAX) + .getForecastsSummary(job, STATUS_FINISHED_QUERY, bounds.min.valueOf(), FORECASTS_VIEW_MAX) .then((resp) => { this.setState({ previousForecasts: resp.forecasts, @@ -558,6 +558,7 @@ export class ForecastingModal extends Component { jobOpeningState={this.state.jobOpeningState} jobClosingState={this.state.jobClosingState} messages={this.state.messages} + selectedForecastId={this.props.selectedForecastId} /> )}
    diff --git a/x-pack/plugins/ml/public/application/timeseriesexplorer/components/forecasting_modal/forecasts_list.js b/x-pack/plugins/ml/public/application/timeseriesexplorer/components/forecasting_modal/forecasts_list.js index 9e01f06094451..52ce2b201dd8d 100644 --- a/x-pack/plugins/ml/public/application/timeseriesexplorer/components/forecasting_modal/forecasts_list.js +++ b/x-pack/plugins/ml/public/application/timeseriesexplorer/components/forecasting_modal/forecasts_list.js @@ -12,11 +12,12 @@ import PropTypes from 'prop-types'; import React from 'react'; -import { EuiButtonIcon, EuiIcon, EuiInMemoryTable, EuiText, EuiToolTip } from '@elastic/eui'; +import { EuiButtonIcon, EuiIconTip, EuiInMemoryTable, EuiText } from '@elastic/eui'; import { formatHumanReadableDateTimeSeconds } from '@kbn/ml-date-utils'; import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n-react'; +import { useCurrentThemeVars } from '../../../contexts/kibana'; function getColumns(viewForecast) { return [ @@ -75,37 +76,41 @@ function getColumns(viewForecast) { ]; } -// TODO - add in ml-info-icon to the h3 element, -// then remove tooltip and inline style. -export function ForecastsList({ forecasts, viewForecast }) { +export function ForecastsList({ forecasts, viewForecast, selectedForecastId }) { + const { euiTheme } = useCurrentThemeVars(); + const getRowProps = (item) => { return { 'data-test-subj': `mlForecastsListRow row-${item.rowId}`, + ...(item.forecast_id === selectedForecastId + ? { + style: { + backgroundColor: `${euiTheme.euiPanelBackgroundColorModifiers.primary}`, + }, + } + : {}), }; }; return ( -

    +

    +   + + } + />

    - - } - > - - 0 && ( - + )} @@ -104,4 +108,5 @@ Modal.propType = { jobOpeningState: PropTypes.number, jobClosingState: PropTypes.number, messages: PropTypes.array, + selectedForecastId: PropTypes.string, }; diff --git a/x-pack/plugins/ml/public/application/timeseriesexplorer/timeseriesexplorer.js b/x-pack/plugins/ml/public/application/timeseriesexplorer/timeseriesexplorer.js index 601bbf058868f..57ded98fc8374 100644 --- a/x-pack/plugins/ml/public/application/timeseriesexplorer/timeseriesexplorer.js +++ b/x-pack/plugins/ml/public/application/timeseriesexplorer/timeseriesexplorer.js @@ -1081,6 +1081,7 @@ export class TimeSeriesExplorer extends React.Component { latestRecordTimestamp={selectedJob.data_counts.latest_record_timestamp} setForecastId={this.setForecastId} className="forecast-controls" + selectedForecastId={this.props.selectedForecastId} />
    diff --git a/x-pack/plugins/ml/public/application/timeseriesexplorer/timeseriesexplorer_embeddable_chart/timeseriesexplorer_embeddable_chart.js b/x-pack/plugins/ml/public/application/timeseriesexplorer/timeseriesexplorer_embeddable_chart/timeseriesexplorer_embeddable_chart.js index 90b0e76167517..48ef63c2eae37 100644 --- a/x-pack/plugins/ml/public/application/timeseriesexplorer/timeseriesexplorer_embeddable_chart/timeseriesexplorer_embeddable_chart.js +++ b/x-pack/plugins/ml/public/application/timeseriesexplorer/timeseriesexplorer_embeddable_chart/timeseriesexplorer_embeddable_chart.js @@ -1051,6 +1051,7 @@ export class TimeSeriesExplorerEmbeddableChart extends React.Component { setForecastId={this.setForecastId} className="forecast-controls" onForecastComplete={onForecastComplete} + selectedForecastId={this.props.selectedForecastId} /> )} diff --git a/x-pack/plugins/ml/public/embeddables/anomaly_swimlane/anomaly_swimlane_embeddable_factory.tsx b/x-pack/plugins/ml/public/embeddables/anomaly_swimlane/anomaly_swimlane_embeddable_factory.tsx index 34390075f927b..464b5bd196675 100644 --- a/x-pack/plugins/ml/public/embeddables/anomaly_swimlane/anomaly_swimlane_embeddable_factory.tsx +++ b/x-pack/plugins/ml/public/embeddables/anomaly_swimlane/anomaly_swimlane_embeddable_factory.tsx @@ -18,6 +18,7 @@ import { apiHasExecutionContext, apiHasParentApi, apiPublishesTimeRange, + fetch$, initializeTimeRange, initializeTitles, useBatchedPublishingSubjects, @@ -26,7 +27,8 @@ import { KibanaRenderContextProvider } from '@kbn/react-kibana-context-render'; import React, { useCallback, useState } from 'react'; import useUnmount from 'react-use/lib/useUnmount'; import type { Observable } from 'rxjs'; -import { BehaviorSubject, combineLatest, map, of, Subscription } from 'rxjs'; +import { BehaviorSubject, combineLatest, distinctUntilChanged, map, of, Subscription } from 'rxjs'; +import fastIsEqual from 'fast-deep-equal'; import type { AnomalySwimlaneEmbeddableServices } from '..'; import { ANOMALY_SWIMLANE_EMBEDDABLE_TYPE } from '..'; import type { MlDependencies } from '../../application/app'; @@ -235,6 +237,21 @@ export const getAnomalySwimLaneEmbeddableFactory = ( anomalySwimLaneServices ); + subscriptions.add( + fetch$(api) + .pipe( + map((fetchContext) => ({ + query: fetchContext.query, + filters: fetchContext.filters, + timeRange: fetchContext.timeRange, + })), + distinctUntilChanged(fastIsEqual) + ) + .subscribe(() => { + api.updatePagination({ fromPage: 1 }); + }) + ); + const onRenderComplete = () => {}; return { diff --git a/x-pack/plugins/ml/public/embeddables/anomaly_swimlane/initialize_swim_lane_data_fetcher.ts b/x-pack/plugins/ml/public/embeddables/anomaly_swimlane/initialize_swim_lane_data_fetcher.ts index 268a17fca4a81..be678af02a65b 100644 --- a/x-pack/plugins/ml/public/embeddables/anomaly_swimlane/initialize_swim_lane_data_fetcher.ts +++ b/x-pack/plugins/ml/public/embeddables/anomaly_swimlane/initialize_swim_lane_data_fetcher.ts @@ -6,7 +6,7 @@ */ import type { estypes } from '@elastic/elasticsearch'; -import type { TimeRange } from '@kbn/es-query'; +import { type TimeRange } from '@kbn/es-query'; import type { PublishesUnifiedSearch } from '@kbn/presentation-publishing'; import { BehaviorSubject, @@ -29,7 +29,6 @@ import { SWIMLANE_TYPE, } from '../../application/explorer/explorer_constants'; import type { OverallSwimlaneData } from '../../application/explorer/explorer_utils'; -import { isViewBySwimLaneData } from '../../application/explorer/swimlane_container'; import { CONTROLLED_BY_SWIM_LANE_FILTER } from '../../ui_actions/constants'; import { getJobsObservable } from '../common/get_jobs_observable'; import { processFilters } from '../common/process_filters'; @@ -114,12 +113,7 @@ export const initializeSwimLaneDataFetcher = ( const { earliest, latest } = overallSwimlaneData; if (overallSwimlaneData && swimlaneType === SWIMLANE_TYPE.VIEW_BY) { - const swimlaneData = swimLaneData$.value; - - let swimLaneLimit = ANOMALY_SWIM_LANE_HARD_LIMIT; - if (isViewBySwimLaneData(swimlaneData) && viewBy === swimlaneData.fieldName) { - swimLaneLimit = swimlaneData.cardinality; - } + const swimLaneLimit = ANOMALY_SWIM_LANE_HARD_LIMIT; return from( anomalyTimelineService.loadViewBySwimlane( diff --git a/x-pack/plugins/ml/server/lib/capabilities/check_capabilities.test.ts b/x-pack/plugins/ml/server/lib/capabilities/check_capabilities.test.ts index d19ff7f723d0a..e82371c358152 100644 --- a/x-pack/plugins/ml/server/lib/capabilities/check_capabilities.test.ts +++ b/x-pack/plugins/ml/server/lib/capabilities/check_capabilities.test.ts @@ -47,7 +47,7 @@ describe('check_capabilities', () => { ); const { capabilities } = await getCapabilities(); const count = Object.keys(capabilities).length; - expect(count).toBe(43); + expect(count).toBe(44); }); }); @@ -86,6 +86,7 @@ describe('check_capabilities', () => { expect(capabilities.canCloseJob).toBe(false); expect(capabilities.canResetJob).toBe(false); expect(capabilities.canForecastJob).toBe(false); + expect(capabilities.canDeleteForecast).toBe(false); expect(capabilities.canStartStopDatafeed).toBe(false); expect(capabilities.canUpdateJob).toBe(false); expect(capabilities.canCreateDatafeed).toBe(false); @@ -146,6 +147,7 @@ describe('check_capabilities', () => { expect(capabilities.canCloseJob).toBe(true); expect(capabilities.canResetJob).toBe(true); expect(capabilities.canForecastJob).toBe(true); + expect(capabilities.canDeleteForecast).toBe(true); expect(capabilities.canStartStopDatafeed).toBe(true); expect(capabilities.canUpdateJob).toBe(true); expect(capabilities.canCreateDatafeed).toBe(true); @@ -206,6 +208,7 @@ describe('check_capabilities', () => { expect(capabilities.canCloseJob).toBe(false); expect(capabilities.canResetJob).toBe(false); expect(capabilities.canForecastJob).toBe(false); + expect(capabilities.canDeleteForecast).toBe(false); expect(capabilities.canStartStopDatafeed).toBe(false); expect(capabilities.canUpdateJob).toBe(false); expect(capabilities.canCreateDatafeed).toBe(false); @@ -266,6 +269,7 @@ describe('check_capabilities', () => { expect(capabilities.canCloseJob).toBe(false); expect(capabilities.canResetJob).toBe(false); expect(capabilities.canForecastJob).toBe(false); + expect(capabilities.canDeleteForecast).toBe(false); expect(capabilities.canStartStopDatafeed).toBe(false); expect(capabilities.canUpdateJob).toBe(false); expect(capabilities.canCreateDatafeed).toBe(false); @@ -326,6 +330,7 @@ describe('check_capabilities', () => { expect(capabilities.canCloseJob).toBe(false); expect(capabilities.canResetJob).toBe(false); expect(capabilities.canForecastJob).toBe(false); + expect(capabilities.canDeleteForecast).toBe(false); expect(capabilities.canStartStopDatafeed).toBe(false); expect(capabilities.canUpdateJob).toBe(false); expect(capabilities.canCreateDatafeed).toBe(false); @@ -387,6 +392,7 @@ describe('check_capabilities', () => { expect(capabilities.canCloseJob).toBe(false); expect(capabilities.canResetJob).toBe(false); expect(capabilities.canForecastJob).toBe(false); + expect(capabilities.canDeleteForecast).toBe(false); expect(capabilities.canStartStopDatafeed).toBe(false); expect(capabilities.canUpdateJob).toBe(false); expect(capabilities.canCreateDatafeed).toBe(false); diff --git a/x-pack/plugins/ml/server/models/model_management/model_provider.test.ts b/x-pack/plugins/ml/server/models/model_management/model_provider.test.ts index 33530bade5fcf..0b9b93720234d 100644 --- a/x-pack/plugins/ml/server/models/model_management/model_provider.test.ts +++ b/x-pack/plugins/ml/server/models/model_management/model_provider.test.ts @@ -89,6 +89,8 @@ describe('modelsProvider', () => { { config: { input: { field_names: ['text_field'] } }, description: 'E5 (EmbEddings from bidirEctional Encoder rEpresentations)', + disclaimer: + 'This E5 model, as defined, hosted, integrated and used in conjunction with our other Elastic Software is covered by our standard warranty.', model_id: '.multilingual-e5-small', default: true, supported: true, @@ -103,6 +105,8 @@ describe('modelsProvider', () => { config: { input: { field_names: ['text_field'] } }, description: 'E5 (EmbEddings from bidirEctional Encoder rEpresentations), optimized for linux-x86_64', + disclaimer: + 'This E5 model, as defined, hosted, integrated and used in conjunction with our other Elastic Software is covered by our standard warranty.', model_id: '.multilingual-e5-small_linux-x86_64', os: 'Linux', recommended: true, @@ -175,6 +179,8 @@ describe('modelsProvider', () => { { config: { input: { field_names: ['text_field'] } }, description: 'E5 (EmbEddings from bidirEctional Encoder rEpresentations)', + disclaimer: + 'This E5 model, as defined, hosted, integrated and used in conjunction with our other Elastic Software is covered by our standard warranty.', model_id: '.multilingual-e5-small', recommended: true, supported: true, @@ -189,6 +195,8 @@ describe('modelsProvider', () => { config: { input: { field_names: ['text_field'] } }, description: 'E5 (EmbEddings from bidirEctional Encoder rEpresentations), optimized for linux-x86_64', + disclaimer: + 'This E5 model, as defined, hosted, integrated and used in conjunction with our other Elastic Software is covered by our standard warranty.', model_id: '.multilingual-e5-small_linux-x86_64', os: 'Linux', supported: false, diff --git a/x-pack/plugins/ml/server/routes/anomaly_detectors.ts b/x-pack/plugins/ml/server/routes/anomaly_detectors.ts index 1fafd467595e9..4f843620003ba 100644 --- a/x-pack/plugins/ml/server/routes/anomaly_detectors.ts +++ b/x-pack/plugins/ml/server/routes/anomaly_detectors.ts @@ -13,6 +13,7 @@ import type { RouteInitialization } from '../types'; import { anomalyDetectionJobSchema, anomalyDetectionUpdateJobSchema, + deleteForecastSchema, jobIdSchema, getBucketsSchema, getOverallBucketsSchema, @@ -379,6 +380,41 @@ export function jobRoutes({ router, routeGuard }: RouteInitialization) { }) ); + router.versioned + .delete({ + path: `${ML_INTERNAL_BASE_PATH}/anomaly_detectors/{jobId}/_forecast/{forecastId}`, + access: 'internal', + options: { + tags: ['access:ml:canDeleteForecast'], + }, + summary: 'Deletes specified forecast for specified job', + description: 'Deletes a specified forecast for the specified anomaly detection job.', + }) + .addVersion( + { + version: '1', + validate: { + request: { + params: deleteForecastSchema, + }, + }, + }, + routeGuard.fullLicenseAPIGuard(async ({ mlClient, request, response }) => { + try { + const { jobId, forecastId } = request.params; + const body = await mlClient.deleteForecast({ + job_id: jobId, + forecast_id: forecastId, + }); + return response.ok({ + body, + }); + } catch (e) { + return response.customError(wrapError(e)); + } + }) + ); + router.versioned .post({ path: `${ML_INTERNAL_BASE_PATH}/anomaly_detectors/{jobId}/_forecast`, diff --git a/x-pack/plugins/ml/server/routes/schemas/anomaly_detectors_schema.ts b/x-pack/plugins/ml/server/routes/schemas/anomaly_detectors_schema.ts index 370b5d657e7c1..3b1eb0b481e46 100644 --- a/x-pack/plugins/ml/server/routes/schemas/anomaly_detectors_schema.ts +++ b/x-pack/plugins/ml/server/routes/schemas/anomaly_detectors_schema.ts @@ -154,6 +154,11 @@ export const jobIdSchema = schema.object({ ...jobIdSchemaBasic, }); +export const deleteForecastSchema = schema.object({ + ...jobIdSchemaBasic, + forecastId: schema.string(), +}); + export const getBucketsSchema = schema.object({ anomaly_score: schema.maybe(schema.number()), desc: schema.maybe(schema.boolean()), diff --git a/x-pack/plugins/observability_solution/apm/common/entities/types.ts b/x-pack/plugins/observability_solution/apm/common/entities/types.ts index 1e13d7c1d3634..9775b1e32eae6 100644 --- a/x-pack/plugins/observability_solution/apm/common/entities/types.ts +++ b/x-pack/plugins/observability_solution/apm/common/entities/types.ts @@ -10,16 +10,3 @@ export enum EntityDataStreamType { TRACES = 'traces', LOGS = 'logs', } - -interface TraceMetrics { - latency?: number | null; - throughput?: number | null; - failedTransactionRate?: number | null; -} - -interface LogsMetrics { - logRate?: number | null; - logErrorRate?: number | null; -} - -export type EntityMetrics = TraceMetrics & LogsMetrics; diff --git a/x-pack/plugins/observability_solution/apm/public/components/app/service_logs/index.tsx b/x-pack/plugins/observability_solution/apm/public/components/app/service_logs/index.tsx index 4df52758ceda3..a1dadbf186b91 100644 --- a/x-pack/plugins/observability_solution/apm/public/components/app/service_logs/index.tsx +++ b/x-pack/plugins/observability_solution/apm/public/components/app/service_logs/index.tsx @@ -5,19 +5,36 @@ * 2.0. */ -import React from 'react'; +import React, { useMemo } from 'react'; import moment from 'moment'; +import { QueryDslQueryContainer } from '@elastic/elasticsearch/lib/api/types'; import { LogStream } from '@kbn/logs-shared-plugin/public'; import { ENVIRONMENT_ALL } from '../../../../common/environment_filter_values'; -import { useFetcher } from '../../../hooks/use_fetcher'; -import { useApmServiceContext } from '../../../context/apm_service/use_apm_service_context'; -import { APIReturnType } from '../../../services/rest/create_call_apm_api'; - import { CONTAINER_ID, SERVICE_ENVIRONMENT, SERVICE_NAME } from '../../../../common/es_fields/apm'; +import { useApmServiceContext } from '../../../context/apm_service/use_apm_service_context'; +import { useKibana } from '../../../context/kibana_context/use_kibana'; import { useAnyOfApmParams } from '../../../hooks/use_apm_params'; +import { FETCH_STATUS, useFetcher } from '../../../hooks/use_fetcher'; import { useTimeRange } from '../../../hooks/use_time_range'; +import { APIReturnType } from '../../../services/rest/create_call_apm_api'; export function ServiceLogs() { + const { + services: { + logsShared: { LogsOverview }, + }, + } = useKibana(); + + const isLogsOverviewEnabled = LogsOverview.useIsEnabled(); + + if (isLogsOverviewEnabled) { + return ; + } else { + return ; + } +} + +export function ClassicServiceLogsStream() { const { serviceName } = useApmServiceContext(); const { @@ -58,6 +75,54 @@ export function ServiceLogs() { ); } +export function ServiceLogsOverview() { + const { + services: { logsShared }, + } = useKibana(); + const { serviceName } = useApmServiceContext(); + const { + query: { environment, kuery, rangeFrom, rangeTo }, + } = useAnyOfApmParams('/services/{serviceName}/logs'); + const { start, end } = useTimeRange({ rangeFrom, rangeTo }); + const timeRange = useMemo(() => ({ start, end }), [start, end]); + + const { data: logFilters, status } = useFetcher( + async (callApmApi) => { + if (start == null || end == null) { + return; + } + + const { containerIds } = await callApmApi( + 'GET /internal/apm/services/{serviceName}/infrastructure_attributes', + { + params: { + path: { serviceName }, + query: { + environment, + kuery, + start, + end, + }, + }, + } + ); + + return [getInfrastructureFilter({ containerIds, environment, serviceName })]; + }, + [environment, kuery, serviceName, start, end] + ); + + if (status === FETCH_STATUS.SUCCESS) { + return ; + } else if (status === FETCH_STATUS.FAILURE) { + return ( + + ); + } else { + return ; + } +} + export function getInfrastructureKQLFilter({ data, serviceName, @@ -84,3 +149,99 @@ export function getInfrastructureKQLFilter({ return [serviceNameAndEnvironmentCorrelation, ...containerIdCorrelation].join(' or '); } + +export function getInfrastructureFilter({ + containerIds, + environment, + serviceName, +}: { + containerIds: string[]; + environment: string; + serviceName: string; +}): QueryDslQueryContainer { + return { + bool: { + should: [ + ...getServiceShouldClauses({ environment, serviceName }), + ...getContainerShouldClauses({ containerIds }), + ], + minimum_should_match: 1, + }, + }; +} + +export function getServiceShouldClauses({ + environment, + serviceName, +}: { + environment: string; + serviceName: string; +}): QueryDslQueryContainer[] { + const serviceNameFilter: QueryDslQueryContainer = { + term: { + [SERVICE_NAME]: serviceName, + }, + }; + + if (environment === ENVIRONMENT_ALL.value) { + return [serviceNameFilter]; + } else { + return [ + { + bool: { + filter: [ + serviceNameFilter, + { + term: { + [SERVICE_ENVIRONMENT]: environment, + }, + }, + ], + }, + }, + { + bool: { + filter: [serviceNameFilter], + must_not: [ + { + exists: { + field: SERVICE_ENVIRONMENT, + }, + }, + ], + }, + }, + ]; + } +} + +export function getContainerShouldClauses({ + containerIds = [], +}: { + containerIds: string[]; +}): QueryDslQueryContainer[] { + if (containerIds.length === 0) { + return []; + } + + return [ + { + bool: { + filter: [ + { + terms: { + [CONTAINER_ID]: containerIds, + }, + }, + ], + must_not: [ + { + term: { + [SERVICE_NAME]: '*', + }, + }, + ], + }, + }, + ]; +} diff --git a/x-pack/plugins/observability_solution/apm/public/components/routing/service_detail/index.tsx b/x-pack/plugins/observability_solution/apm/public/components/routing/service_detail/index.tsx index d746e0464fd40..8a4a1c32877c5 100644 --- a/x-pack/plugins/observability_solution/apm/public/components/routing/service_detail/index.tsx +++ b/x-pack/plugins/observability_solution/apm/public/components/routing/service_detail/index.tsx @@ -330,7 +330,7 @@ export const serviceDetailRoute = { }), element: , searchBarOptions: { - showUnifiedSearchBar: false, + showQueryInput: false, }, }), '/services/{serviceName}/infrastructure': { diff --git a/x-pack/plugins/observability_solution/apm/public/plugin.ts b/x-pack/plugins/observability_solution/apm/public/plugin.ts index 9a9f45f42a39e..b21bdedac9ef8 100644 --- a/x-pack/plugins/observability_solution/apm/public/plugin.ts +++ b/x-pack/plugins/observability_solution/apm/public/plugin.ts @@ -69,6 +69,7 @@ import { from } from 'rxjs'; import { map } from 'rxjs'; import type { CloudSetup } from '@kbn/cloud-plugin/public'; import type { ServerlessPluginStart } from '@kbn/serverless/public'; +import { LogsSharedClientStartExports } from '@kbn/logs-shared-plugin/public'; import type { ConfigSchema } from '.'; import { registerApmRuleTypes } from './components/alerting/rule_types/register_apm_rule_types'; import { registerEmbeddables } from './embeddable/register_embeddables'; @@ -142,6 +143,7 @@ export interface ApmPluginStartDeps { dashboard: DashboardStart; metricsDataAccess: MetricsDataPluginStart; uiSettings: IUiSettingsClient; + logsShared: LogsSharedClientStartExports; } const applicationsTitle = i18n.translate('xpack.apm.navigation.rootTitle', { diff --git a/x-pack/plugins/observability_solution/apm/server/routes/entities/get_entities.ts b/x-pack/plugins/observability_solution/apm/server/routes/entities/get_entities.ts index 7395f639bb6e9..6cedb09efa7c2 100644 --- a/x-pack/plugins/observability_solution/apm/server/routes/entities/get_entities.ts +++ b/x-pack/plugins/observability_solution/apm/server/routes/entities/get_entities.ts @@ -12,7 +12,6 @@ import { import type { EntitiesESClient } from '../../lib/helpers/create_es_client/create_entities_es_client/create_entities_es_client'; import { getEntityLatestServices } from './get_entity_latest_services'; import type { EntityLatestServiceRaw } from './types'; -import { getEntityHistoryServicesMetrics } from './get_entity_history_services_metrics'; export function entitiesRangeQuery(start?: number, end?: number): QueryDslQueryContainer[] { if (!start || !end) { @@ -64,30 +63,5 @@ export async function getEntities({ serviceName, }); - const serviceEntitiesHistoryMetricsMap = entityLatestServices.length - ? await getEntityHistoryServicesMetrics({ - start, - end, - entitiesESClient, - entityIds: entityLatestServices.map((latestEntity) => latestEntity.entity.id), - size, - }) - : undefined; - - return entityLatestServices.map((latestEntity) => { - const historyEntityMetrics = serviceEntitiesHistoryMetricsMap?.[latestEntity.entity.id]; - return { - ...latestEntity, - entity: { - ...latestEntity.entity, - metrics: historyEntityMetrics || { - latency: undefined, - logErrorRate: undefined, - failedTransactionRate: undefined, - logRate: undefined, - throughput: undefined, - }, - }, - }; - }); + return entityLatestServices; } diff --git a/x-pack/plugins/observability_solution/apm/server/routes/entities/get_entity_history_services_metrics.ts b/x-pack/plugins/observability_solution/apm/server/routes/entities/get_entity_history_services_metrics.ts deleted file mode 100644 index 009f384af901b..0000000000000 --- a/x-pack/plugins/observability_solution/apm/server/routes/entities/get_entity_history_services_metrics.ts +++ /dev/null @@ -1,84 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { rangeQuery, termsQuery } from '@kbn/observability-plugin/server'; -import { - ENTITY_ID, - ENTITY_LAST_SEEN, -} from '@kbn/observability-shared-plugin/common/field_names/elasticsearch'; -import { EntityMetrics } from '../../../common/entities/types'; -import { - ENTITY_METRICS_FAILED_TRANSACTION_RATE, - ENTITY_METRICS_LATENCY, - ENTITY_METRICS_LOG_ERROR_RATE, - ENTITY_METRICS_LOG_RATE, - ENTITY_METRICS_THROUGHPUT, -} from '../../../common/es_fields/entities'; -import { EntitiesESClient } from '../../lib/helpers/create_es_client/create_entities_es_client/create_entities_es_client'; - -interface Params { - entitiesESClient: EntitiesESClient; - start: number; - end: number; - entityIds: string[]; - size: number; -} - -export async function getEntityHistoryServicesMetrics({ - end, - entityIds, - start, - entitiesESClient, - size, -}: Params) { - const response = await entitiesESClient.searchHistory('get_entities_history', { - body: { - size: 0, - track_total_hits: false, - query: { - bool: { - filter: [ - ...rangeQuery(start, end, ENTITY_LAST_SEEN), - ...termsQuery(ENTITY_ID, ...entityIds), - ], - }, - }, - aggs: { - entityIds: { - terms: { field: ENTITY_ID, size }, - aggs: { - latency: { avg: { field: ENTITY_METRICS_LATENCY } }, - logErrorRate: { avg: { field: ENTITY_METRICS_LOG_ERROR_RATE } }, - logRate: { avg: { field: ENTITY_METRICS_LOG_RATE } }, - throughput: { avg: { field: ENTITY_METRICS_THROUGHPUT } }, - failedTransactionRate: { avg: { field: ENTITY_METRICS_FAILED_TRANSACTION_RATE } }, - }, - }, - }, - }, - }); - - if (!response.aggregations) { - return {}; - } - - return response.aggregations.entityIds.buckets.reduce>( - (acc, currBucket) => { - return { - ...acc, - [currBucket.key]: { - latency: currBucket.latency.value, - logErrorRate: currBucket.logErrorRate.value, - logRate: currBucket.logRate.value, - throughput: currBucket.throughput.value, - failedTransactionRate: currBucket.failedTransactionRate.value, - }, - }; - }, - {} - ); -} diff --git a/x-pack/plugins/observability_solution/apm/server/routes/entities/get_entity_history_services_timeseries.ts b/x-pack/plugins/observability_solution/apm/server/routes/entities/get_entity_history_services_timeseries.ts deleted file mode 100644 index 4ee23966f282c..0000000000000 --- a/x-pack/plugins/observability_solution/apm/server/routes/entities/get_entity_history_services_timeseries.ts +++ /dev/null @@ -1,116 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { getBucketSize } from '@kbn/apm-data-access-plugin/common'; -import { rangeQuery, termsQuery } from '@kbn/observability-plugin/server'; -import { ENTITY_LAST_SEEN } from '@kbn/observability-shared-plugin/common/field_names/elasticsearch'; -import { keyBy } from 'lodash'; -import { SERVICE_NAME } from '../../../common/es_fields/apm'; -import { - ENTITY_METRICS_FAILED_TRANSACTION_RATE, - ENTITY_METRICS_LATENCY, - ENTITY_METRICS_LOG_ERROR_RATE, - ENTITY_METRICS_LOG_RATE, - ENTITY_METRICS_THROUGHPUT, -} from '../../../common/es_fields/entities'; -import { environmentQuery } from '../../../common/utils/environment_query'; -import { EntitiesESClient } from '../../lib/helpers/create_es_client/create_entities_es_client/create_entities_es_client'; - -interface Params { - entitiesESClient: EntitiesESClient; - start: number; - end: number; - serviceNames: string[]; - environment: string; -} - -export async function getEntityHistoryServicesTimeseries({ - start, - end, - serviceNames, - entitiesESClient, - environment, -}: Params) { - const { intervalString } = getBucketSize({ - start, - end, - minBucketSize: 60, - }); - - const response = await entitiesESClient.searchHistory('get_entities_history_timeseries', { - body: { - size: 0, - track_total_hits: false, - query: { - bool: { - filter: [ - ...rangeQuery(start, end, ENTITY_LAST_SEEN), - ...termsQuery(SERVICE_NAME, ...serviceNames), - ...environmentQuery(environment), - ], - }, - }, - aggs: { - serviceNames: { - terms: { field: SERVICE_NAME, size: serviceNames.length }, - aggs: { - timeseries: { - date_histogram: { - field: '@timestamp', - fixed_interval: intervalString, - min_doc_count: 0, - extended_bounds: { min: start, max: end }, - }, - aggs: { - latency: { avg: { field: ENTITY_METRICS_LATENCY } }, - logErrorRate: { avg: { field: ENTITY_METRICS_LOG_ERROR_RATE } }, - logRate: { avg: { field: ENTITY_METRICS_LOG_RATE } }, - throughput: { avg: { field: ENTITY_METRICS_THROUGHPUT } }, - failedTransactionRate: { avg: { field: ENTITY_METRICS_FAILED_TRANSACTION_RATE } }, - }, - }, - }, - }, - }, - }, - }); - - if (!response.aggregations) { - return {}; - } - - return keyBy( - response.aggregations.serviceNames.buckets.map((serviceBucket) => { - const serviceName = serviceBucket.key as string; - - return { - serviceName, - latency: serviceBucket.timeseries.buckets.map((bucket) => ({ - x: bucket.key, - y: bucket.latency.value ?? null, - })), - logErrorRate: serviceBucket.timeseries.buckets.map((bucket) => ({ - x: bucket.key, - y: bucket.logErrorRate.value ?? null, - })), - logRate: serviceBucket.timeseries.buckets.map((bucket) => ({ - x: bucket.key, - y: bucket.logRate.value ?? null, - })), - throughput: serviceBucket.timeseries.buckets.map((bucket) => ({ - x: bucket.key, - y: bucket.throughput.value ?? null, - })), - failedTransactionRate: serviceBucket.timeseries.buckets.map((bucket) => ({ - x: bucket.key, - y: bucket.failedTransactionRate.value ?? null, - })), - }; - }), - 'serviceName' - ); -} diff --git a/x-pack/plugins/observability_solution/apm/server/routes/entities/services/get_service_entities.ts b/x-pack/plugins/observability_solution/apm/server/routes/entities/services/get_service_entities.ts index 084fbbe438952..9e6bb34bceafe 100644 --- a/x-pack/plugins/observability_solution/apm/server/routes/entities/services/get_service_entities.ts +++ b/x-pack/plugins/observability_solution/apm/server/routes/entities/services/get_service_entities.ts @@ -10,7 +10,6 @@ import { WrappedElasticsearchClientError } from '@kbn/observability-plugin/serve import { EntitiesESClient } from '../../../lib/helpers/create_es_client/create_entities_es_client/create_entities_es_client'; import { withApmSpan } from '../../../utils/with_apm_span'; import { getEntities } from '../get_entities'; -import { calculateAvgMetrics } from '../utils/calculate_avg_metrics'; import { mergeEntities } from '../utils/merge_entities'; export const MAX_NUMBER_OF_SERVICES = 1_000; @@ -41,7 +40,7 @@ export async function getServiceEntities({ size: MAX_NUMBER_OF_SERVICES, }); - return calculateAvgMetrics(mergeEntities({ entities })); + return mergeEntities({ entities }); } catch (error) { // If the index does not exist, handle it gracefully if ( diff --git a/x-pack/plugins/observability_solution/apm/server/routes/entities/services/get_service_entity_summary.ts b/x-pack/plugins/observability_solution/apm/server/routes/entities/services/get_service_entity_summary.ts index e99ecc0217ed9..3ab3b907f5be2 100644 --- a/x-pack/plugins/observability_solution/apm/server/routes/entities/services/get_service_entity_summary.ts +++ b/x-pack/plugins/observability_solution/apm/server/routes/entities/services/get_service_entity_summary.ts @@ -8,7 +8,6 @@ import type { EntitiesESClient } from '../../../lib/helpers/create_es_client/create_entities_es_client/create_entities_es_client'; import { withApmSpan } from '../../../utils/with_apm_span'; import { getEntityLatestServices } from '../get_entity_latest_services'; -import { calculateAvgMetrics } from '../utils/calculate_avg_metrics'; import { mergeEntities } from '../utils/merge_entities'; import { MAX_NUMBER_OF_SERVICES } from './get_service_entities'; @@ -27,7 +26,7 @@ export function getServiceEntitySummary({ entitiesESClient, environment, service serviceName, }); - const serviceEntity = calculateAvgMetrics(mergeEntities({ entities: entityLatestServices })); + const serviceEntity = mergeEntities({ entities: entityLatestServices }); return serviceEntity[0]; }); } diff --git a/x-pack/plugins/observability_solution/apm/server/routes/entities/services/routes.ts b/x-pack/plugins/observability_solution/apm/server/routes/entities/services/routes.ts index c3f36a6d86c52..218de180cbc00 100644 --- a/x-pack/plugins/observability_solution/apm/server/routes/entities/services/routes.ts +++ b/x-pack/plugins/observability_solution/apm/server/routes/entities/services/routes.ts @@ -4,8 +4,6 @@ * 2.0; you may not use this file except in compliance with the Elastic License * 2.0. */ -import Boom from '@hapi/boom'; -import { jsonRt } from '@kbn/io-ts-utils'; import * as t from 'io-ts'; import { environmentQuery } from '../../../../common/utils/environment_query'; import { createEntitiesESClient } from '../../../lib/helpers/create_es_client/create_entities_es_client/create_entities_es_client'; @@ -13,7 +11,6 @@ import { createApmServerRoute } from '../../apm_routes/create_apm_server_route'; import { environmentRt, kueryRt, rangeRt } from '../../default_api_types'; import { getServiceEntities } from './get_service_entities'; import { getServiceEntitySummary } from './get_service_entity_summary'; -import { getEntityHistoryServicesTimeseries } from '../get_entity_history_services_timeseries'; const serviceEntitiesSummaryRoute = createApmServerRoute({ endpoint: 'GET /internal/apm/entities/services/{serviceName}/summary', @@ -72,46 +69,6 @@ const servicesEntitiesRoute = createApmServerRoute({ }, }); -const servicesEntitiesDetailedStatisticsRoute = createApmServerRoute({ - endpoint: 'POST /internal/apm/entities/services/detailed_statistics', - params: t.type({ - query: t.intersection([environmentRt, kueryRt, rangeRt]), - body: t.type({ serviceNames: jsonRt.pipe(t.array(t.string)) }), - }), - options: { tags: ['access:apm'] }, - handler: async (resources) => { - const { context, params, request } = resources; - const coreContext = await context.core; - - const entitiesESClient = await createEntitiesESClient({ - request, - esClient: coreContext.elasticsearch.client.asCurrentUser, - }); - - const { environment, start, end } = params.query; - - const { serviceNames } = params.body; - - if (!serviceNames.length) { - throw Boom.badRequest(`serviceNames cannot be empty`); - } - - const serviceEntitiesTimeseries = await getEntityHistoryServicesTimeseries({ - start, - end, - serviceNames, - environment, - entitiesESClient, - }); - - return { - currentPeriod: { - ...serviceEntitiesTimeseries, - }, - }; - }, -}); - const serviceLogRateTimeseriesRoute = createApmServerRoute({ endpoint: 'GET /internal/apm/entities/services/{serviceName}/logs_rate_timeseries', params: t.type({ @@ -183,6 +140,5 @@ export const servicesEntitiesRoutesRepository = { ...servicesEntitiesRoute, ...serviceLogRateTimeseriesRoute, ...serviceLogErrorRateTimeseriesRoute, - ...servicesEntitiesDetailedStatisticsRoute, ...serviceEntitiesSummaryRoute, }; diff --git a/x-pack/plugins/observability_solution/apm/server/routes/entities/types.ts b/x-pack/plugins/observability_solution/apm/server/routes/entities/types.ts index 00f1b0580a669..5713c0a2b67fb 100644 --- a/x-pack/plugins/observability_solution/apm/server/routes/entities/types.ts +++ b/x-pack/plugins/observability_solution/apm/server/routes/entities/types.ts @@ -5,7 +5,6 @@ * 2.0. */ import { AgentName } from '../../../typings/es_schemas/ui/fields/agent'; -import { EntityMetrics } from '../../../common/entities/types'; export enum EntityType { SERVICE = 'service', @@ -30,5 +29,4 @@ interface Entity { lastSeenTimestamp: string; firstSeenTimestamp: string; identityFields: string[]; - metrics: EntityMetrics; } diff --git a/x-pack/plugins/observability_solution/apm/server/routes/entities/utils/calculate_avg_metrics.test.ts b/x-pack/plugins/observability_solution/apm/server/routes/entities/utils/calculate_avg_metrics.test.ts deleted file mode 100644 index 4d0a562295224..0000000000000 --- a/x-pack/plugins/observability_solution/apm/server/routes/entities/utils/calculate_avg_metrics.test.ts +++ /dev/null @@ -1,236 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { EntityMetrics, EntityDataStreamType } from '../../../../common/entities/types'; -import { AgentName } from '../../../../typings/es_schemas/ui/fields/agent'; -import { calculateAvgMetrics, mergeMetrics } from './calculate_avg_metrics'; - -describe('calculateAverageMetrics', () => { - it('calculates average metrics', () => { - const entities = [ - { - agentName: 'nodejs' as AgentName, - dataStreamTypes: [EntityDataStreamType.METRICS, EntityDataStreamType.LOGS], - environments: [], - latestTimestamp: '2024-03-05T10:34:40.810Z', - metrics: [ - { - failedTransactionRate: 5, - latency: 5, - logErrorRate: 5, - logRate: 5, - throughput: 5, - }, - { - failedTransactionRate: 10, - latency: 10, - logErrorRate: 10, - logRate: 10, - throughput: 10, - }, - ], - serviceName: 'service-1', - hasLogMetrics: true, - }, - { - agentName: 'java' as AgentName, - dataStreamTypes: [EntityDataStreamType.METRICS], - environments: [], - latestTimestamp: '2024-06-05T10:34:40.810Z', - metrics: [ - { - failedTransactionRate: 15, - latency: 15, - logErrorRate: 15, - logRate: 15, - throughput: 15, - }, - { - failedTransactionRate: 5, - latency: 5, - logErrorRate: 5, - logRate: 5, - throughput: 5, - }, - ], - serviceName: 'service-2', - hasLogMetrics: true, - }, - ]; - - const result = calculateAvgMetrics(entities); - - expect(result).toEqual([ - { - agentName: 'nodejs', - dataStreamTypes: [EntityDataStreamType.METRICS, EntityDataStreamType.LOGS], - environments: [], - latestTimestamp: '2024-03-05T10:34:40.810Z', - metrics: { - failedTransactionRate: 7.5, - latency: 7.5, - logErrorRate: 7.5, - logRate: 7.5, - throughput: 7.5, - }, - serviceName: 'service-1', - hasLogMetrics: true, - }, - { - agentName: 'java' as AgentName, - dataStreamTypes: [EntityDataStreamType.METRICS], - environments: [], - latestTimestamp: '2024-06-05T10:34:40.810Z', - metrics: { - failedTransactionRate: 10, - latency: 10, - logErrorRate: 10, - logRate: 10, - throughput: 10, - }, - serviceName: 'service-2', - hasLogMetrics: true, - }, - ]); - }); - it('calculates average metrics with null', () => { - const entities = [ - { - agentName: 'nodejs' as AgentName, - dataStreamTypes: [EntityDataStreamType.METRICS], - environments: ['env-service-1', 'env-service-2'], - latestTimestamp: '2024-03-05T10:34:40.810Z', - metrics: [ - { - failedTransactionRate: 5, - latency: null, - logErrorRate: 5, - logRate: 5, - throughput: 5, - }, - { - failedTransactionRate: 10, - latency: null, - logErrorRate: 10, - logRate: 10, - throughput: 10, - }, - ], - serviceName: 'service-1', - hasLogMetrics: true, - }, - ]; - - const result = calculateAvgMetrics(entities); - - expect(result).toEqual([ - { - agentName: 'nodejs', - dataStreamTypes: [EntityDataStreamType.METRICS], - environments: ['env-service-1', 'env-service-2'], - latestTimestamp: '2024-03-05T10:34:40.810Z', - metrics: { - failedTransactionRate: 7.5, - logErrorRate: 7.5, - logRate: 7.5, - throughput: 7.5, - }, - serviceName: 'service-1', - hasLogMetrics: true, - }, - ]); - }); -}); - -describe('mergeMetrics', () => { - it('merges metrics correctly', () => { - const metrics = [ - { - failedTransactionRate: 5, - latency: 5, - logErrorRate: 5, - logRate: 5, - throughput: 5, - }, - { - failedTransactionRate: 10, - latency: 10, - logErrorRate: 10, - logRate: 10, - throughput: 10, - }, - ]; - - const result = mergeMetrics(metrics); - - expect(result).toEqual({ - failedTransactionRate: [5, 10], - latency: [5, 10], - logErrorRate: [5, 10], - logRate: [5, 10], - throughput: [5, 10], - }); - }); - - it('handles empty metrics array', () => { - const metrics: EntityMetrics[] = []; - - const result = mergeMetrics(metrics); - - expect(result).toEqual({}); - }); - - it('returns metrics with zero value', () => { - const metrics = [ - { - failedTransactionRate: 0, - latency: 4, - logErrorRate: 5, - logRate: 5, - throughput: 5, - }, - ]; - - const result = mergeMetrics(metrics); - - expect(result).toEqual({ - failedTransactionRate: [0], - latency: [4], - logErrorRate: [5], - logRate: [5], - throughput: [5], - }); - }); - - it('does not return metrics with null', () => { - const metrics = [ - { - failedTransactionRate: null, - latency: null, - logErrorRate: 5, - logRate: 5, - throughput: 5, - }, - { - failedTransactionRate: 5, - latency: null, - logErrorRate: 5, - logRate: 5, - throughput: 5, - }, - ]; - - const result = mergeMetrics(metrics); - - expect(result).toEqual({ - failedTransactionRate: [5], - logErrorRate: [5, 5], - logRate: [5, 5], - throughput: [5, 5], - }); - }); -}); diff --git a/x-pack/plugins/observability_solution/apm/server/routes/entities/utils/calculate_avg_metrics.ts b/x-pack/plugins/observability_solution/apm/server/routes/entities/utils/calculate_avg_metrics.ts deleted file mode 100644 index bee5f7ce9cc2b..0000000000000 --- a/x-pack/plugins/observability_solution/apm/server/routes/entities/utils/calculate_avg_metrics.ts +++ /dev/null @@ -1,45 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { mapValues, isNumber } from 'lodash'; -import { EntityMetrics } from '../../../../common/entities/types'; -import type { MergedServiceEntity } from './merge_entities'; - -export function calculateAvgMetrics(entities: MergedServiceEntity[]) { - return entities.map((entity) => { - const transformedMetrics = mergeMetrics(entity.metrics); - const averages = mapValues(transformedMetrics, (values: number[]) => { - const sum = values.reduce((acc: number, val: number) => acc + (val !== null ? val : 0), 0); - return sum / values.length; - }); - - return { - ...entity, - metrics: averages, - }; - }); -} -type MetricsKey = keyof EntityMetrics; - -export function mergeMetrics(metrics: EntityMetrics[]) { - return metrics.reduce((acc, metric) => { - for (const key in metric) { - if (Object.hasOwn(metric, key)) { - const metricsKey = key as MetricsKey; - - const value = metric[metricsKey]; - if (isNumber(value)) { - if (!acc[metricsKey]) { - acc[metricsKey] = []; - } - acc[metricsKey].push(value); - } - } - } - return acc; - }, {} as { [key in MetricsKey]: number[] }); -} diff --git a/x-pack/plugins/observability_solution/apm/server/routes/entities/utils/merge_entities.test.ts b/x-pack/plugins/observability_solution/apm/server/routes/entities/utils/merge_entities.test.ts index 587cb03975105..bb5c4f48b4125 100644 --- a/x-pack/plugins/observability_solution/apm/server/routes/entities/utils/merge_entities.test.ts +++ b/x-pack/plugins/observability_solution/apm/server/routes/entities/utils/merge_entities.test.ts @@ -22,13 +22,6 @@ describe('mergeEntities', () => { entity: { firstSeenTimestamp: '2024-06-05T10:34:40.810Z', lastSeenTimestamp: '2024-06-05T10:34:40.810Z', - metrics: { - logRate: 1, - logErrorRate: null, - throughput: 0, - failedTransactionRate: 0.3333333333333333, - latency: 10, - }, identityFields: ['service.name', 'service.environment'], id: 'service-1:test', }, @@ -41,16 +34,6 @@ describe('mergeEntities', () => { dataStreamTypes: ['metrics', 'logs'], environments: ['test'], lastSeenTimestamp: '2024-06-05T10:34:40.810Z', - hasLogMetrics: true, - metrics: [ - { - failedTransactionRate: 0.3333333333333333, - latency: 10, - logErrorRate: null, - logRate: 1, - throughput: 0, - }, - ], serviceName: 'service-1', }, ]); @@ -68,13 +51,6 @@ describe('mergeEntities', () => { entity: { firstSeenTimestamp: '2024-03-05T10:34:40.810Z', lastSeenTimestamp: '2024-03-05T10:34:40.810Z', - metrics: { - logRate: 1, - logErrorRate: null, - throughput: 0, - failedTransactionRate: 0.3333333333333333, - latency: 10, - }, identityFields: ['service.name', 'service.environment'], id: 'service-1:env-service-1', }, @@ -89,13 +65,6 @@ describe('mergeEntities', () => { entity: { firstSeenTimestamp: '2024-03-05T10:34:40.810Z', lastSeenTimestamp: '2024-03-05T10:34:40.810Z', - metrics: { - logRate: 10, - logErrorRate: 10, - throughput: 10, - failedTransactionRate: 10, - latency: 10, - }, identityFields: ['service.name', 'service.environment'], id: 'apm-only-1:synthtrace-env-2', }, @@ -110,13 +79,6 @@ describe('mergeEntities', () => { entity: { firstSeenTimestamp: '2024-06-05T10:34:40.810Z', lastSeenTimestamp: '2024-06-05T10:34:40.810Z', - metrics: { - logRate: 15, - logErrorRate: 15, - throughput: 15, - failedTransactionRate: 15, - latency: 15, - }, identityFields: ['service.name', 'service.environment'], id: 'service-2:env-service-3', }, @@ -131,13 +93,6 @@ describe('mergeEntities', () => { entity: { firstSeenTimestamp: '2024-06-05T10:34:40.810Z', lastSeenTimestamp: '2024-06-05T10:34:40.810Z', - metrics: { - logRate: 5, - logErrorRate: 5, - throughput: 5, - failedTransactionRate: 5, - latency: 5, - }, identityFields: ['service.name', 'service.environment'], id: 'service-2:env-service-3', }, @@ -151,23 +106,6 @@ describe('mergeEntities', () => { dataStreamTypes: ['foo', 'bar'], environments: ['env-service-1', 'env-service-2'], lastSeenTimestamp: '2024-03-05T10:34:40.810Z', - hasLogMetrics: true, - metrics: [ - { - failedTransactionRate: 0.3333333333333333, - latency: 10, - logErrorRate: null, - logRate: 1, - throughput: 0, - }, - { - failedTransactionRate: 10, - latency: 10, - logErrorRate: 10, - logRate: 10, - throughput: 10, - }, - ], serviceName: 'service-1', }, { @@ -175,23 +113,6 @@ describe('mergeEntities', () => { dataStreamTypes: ['baz'], environments: ['env-service-3', 'env-service-4'], lastSeenTimestamp: '2024-06-05T10:34:40.810Z', - hasLogMetrics: true, - metrics: [ - { - failedTransactionRate: 15, - latency: 15, - logErrorRate: 15, - logRate: 15, - throughput: 15, - }, - { - failedTransactionRate: 5, - latency: 5, - logErrorRate: 5, - logRate: 5, - throughput: 5, - }, - ], serviceName: 'service-2', }, ]); @@ -208,13 +129,6 @@ describe('mergeEntities', () => { entity: { firstSeenTimestamp: '2024-06-05T10:34:40.810Z', lastSeenTimestamp: '2024-06-05T10:34:40.810Z', - metrics: { - logRate: 5, - logErrorRate: 5, - throughput: 5, - failedTransactionRate: 5, - latency: 5, - }, identityFields: ['service.name', 'service.environment'], id: 'service-1:test', }, @@ -229,13 +143,6 @@ describe('mergeEntities', () => { entity: { firstSeenTimestamp: '2024-06-05T10:34:40.810Z', lastSeenTimestamp: '2024-06-05T10:34:40.810Z', - metrics: { - logRate: 10, - logErrorRate: 10, - throughput: 10, - failedTransactionRate: 0.3333333333333333, - latency: 10, - }, identityFields: ['service.name', 'service.environment'], id: 'service-1:test', }, @@ -250,13 +157,6 @@ describe('mergeEntities', () => { entity: { firstSeenTimestamp: '2024-23-05T10:34:40.810Z', lastSeenTimestamp: '2024-23-05T10:34:40.810Z', - metrics: { - logRate: 0.333, - logErrorRate: 0.333, - throughput: 0.333, - failedTransactionRate: 0.333, - latency: 0.333, - }, identityFields: ['service.name', 'service.environment'], id: 'service-1:prod', }, @@ -269,30 +169,6 @@ describe('mergeEntities', () => { dataStreamTypes: ['metrics', 'logs', 'foo'], environments: ['test', 'prod'], lastSeenTimestamp: '2024-23-05T10:34:40.810Z', - hasLogMetrics: true, - metrics: [ - { - failedTransactionRate: 5, - latency: 5, - logErrorRate: 5, - logRate: 5, - throughput: 5, - }, - { - failedTransactionRate: 0.3333333333333333, - latency: 10, - logErrorRate: 10, - logRate: 10, - throughput: 10, - }, - { - failedTransactionRate: 0.333, - latency: 0.333, - logErrorRate: 0.333, - logRate: 0.333, - throughput: 0.333, - }, - ], serviceName: 'service-1', }, ]); @@ -309,13 +185,6 @@ describe('mergeEntities', () => { entity: { firstSeenTimestamp: '2024-06-05T10:34:40.810Z', lastSeenTimestamp: '2024-06-05T10:34:40.810Z', - metrics: { - logRate: 1, - logErrorRate: null, - throughput: 0, - failedTransactionRate: 0.3333333333333333, - latency: 10, - }, identityFields: ['service.name'], id: 'service-1:test', }, @@ -328,16 +197,6 @@ describe('mergeEntities', () => { dataStreamTypes: [], environments: [], lastSeenTimestamp: '2024-06-05T10:34:40.810Z', - hasLogMetrics: true, - metrics: [ - { - failedTransactionRate: 0.3333333333333333, - latency: 10, - logErrorRate: null, - logRate: 1, - throughput: 0, - }, - ], serviceName: 'service-1', }, ]); @@ -352,13 +211,6 @@ describe('mergeEntities', () => { entity: { firstSeenTimestamp: '2024-06-05T10:34:40.810Z', lastSeenTimestamp: '2024-06-05T10:34:40.810Z', - metrics: { - logRate: 1, - logErrorRate: null, - throughput: 0, - failedTransactionRate: 0.3333333333333333, - latency: 10, - }, identityFields: ['service.name'], id: 'service-1:test', }, @@ -372,13 +224,6 @@ describe('mergeEntities', () => { entity: { firstSeenTimestamp: '2024-06-05T10:34:40.810Z', lastSeenTimestamp: '2024-06-05T10:34:40.810Z', - metrics: { - logRate: 1, - logErrorRate: null, - throughput: 0, - failedTransactionRate: 0.3333333333333333, - latency: 10, - }, identityFields: ['service.name'], id: 'service-1:test', }, @@ -391,23 +236,6 @@ describe('mergeEntities', () => { dataStreamTypes: [], environments: [], lastSeenTimestamp: '2024-06-05T10:34:40.810Z', - hasLogMetrics: true, - metrics: [ - { - failedTransactionRate: 0.3333333333333333, - latency: 10, - logErrorRate: null, - logRate: 1, - throughput: 0, - }, - { - logRate: 1, - logErrorRate: null, - throughput: 0, - failedTransactionRate: 0.3333333333333333, - latency: 10, - }, - ], serviceName: 'service-1', }, ]); @@ -424,13 +252,6 @@ describe('mergeEntities', () => { entity: { firstSeenTimestamp: '2024-06-05T10:34:40.810Z', lastSeenTimestamp: '2024-06-05T10:34:40.810Z', - metrics: { - logRate: 1, - logErrorRate: null, - throughput: 0, - failedTransactionRate: 0.3333333333333333, - latency: 10, - }, identityFields: ['service.name'], id: 'service-1:test', }, @@ -443,16 +264,6 @@ describe('mergeEntities', () => { dataStreamTypes: [], environments: [], lastSeenTimestamp: '2024-06-05T10:34:40.810Z', - hasLogMetrics: true, - metrics: [ - { - failedTransactionRate: 0.3333333333333333, - latency: 10, - logErrorRate: null, - logRate: 1, - throughput: 0, - }, - ], serviceName: 'service-1', }, ]); @@ -467,13 +278,6 @@ describe('mergeEntities', () => { entity: { firstSeenTimestamp: '2024-06-05T10:34:40.810Z', lastSeenTimestamp: '2024-06-05T10:34:40.810Z', - metrics: { - logRate: 1, - logErrorRate: null, - throughput: 0, - failedTransactionRate: 0.3333333333333333, - latency: 10, - }, identityFields: ['service.name'], id: 'service-1:test', }, @@ -487,13 +291,6 @@ describe('mergeEntities', () => { entity: { firstSeenTimestamp: '2024-06-05T10:34:40.810Z', lastSeenTimestamp: '2024-06-05T10:34:40.810Z', - metrics: { - logRate: 1, - logErrorRate: null, - throughput: 0, - failedTransactionRate: 0.3333333333333333, - latency: 10, - }, identityFields: ['service.name'], id: 'service-1:test', }, @@ -506,23 +303,6 @@ describe('mergeEntities', () => { dataStreamTypes: [], environments: [], lastSeenTimestamp: '2024-06-05T10:34:40.810Z', - hasLogMetrics: true, - metrics: [ - { - failedTransactionRate: 0.3333333333333333, - latency: 10, - logErrorRate: null, - logRate: 1, - throughput: 0, - }, - { - logRate: 1, - logErrorRate: null, - throughput: 0, - failedTransactionRate: 0.3333333333333333, - latency: 10, - }, - ], serviceName: 'service-1', }, ]); @@ -540,11 +320,6 @@ describe('mergeEntities', () => { entity: { firstSeenTimestamp: '2024-06-05T10:34:40.810Z', lastSeenTimestamp: '2024-06-05T10:34:40.810Z', - metrics: { - throughput: 0, - failedTransactionRate: 0.3333333333333333, - latency: 10, - }, identityFields: ['service.name', 'service.environment'], id: 'service-1:test', }, @@ -557,14 +332,6 @@ describe('mergeEntities', () => { dataStreamTypes: ['metrics'], environments: ['test'], lastSeenTimestamp: '2024-06-05T10:34:40.810Z', - hasLogMetrics: false, - metrics: [ - { - failedTransactionRate: 0.3333333333333333, - latency: 10, - throughput: 0, - }, - ], serviceName: 'service-1', }, ]); diff --git a/x-pack/plugins/observability_solution/apm/server/routes/entities/utils/merge_entities.ts b/x-pack/plugins/observability_solution/apm/server/routes/entities/utils/merge_entities.ts index 4017d922d63c5..c7269989a3564 100644 --- a/x-pack/plugins/observability_solution/apm/server/routes/entities/utils/merge_entities.ts +++ b/x-pack/plugins/observability_solution/apm/server/routes/entities/utils/merge_entities.ts @@ -7,17 +7,14 @@ import { compact, uniq } from 'lodash'; import type { EntityLatestServiceRaw } from '../types'; -import { isFiniteNumber } from '../../../../common/utils/is_finite_number'; import type { AgentName } from '../../../../typings/es_schemas/ui/fields/agent'; -import type { EntityDataStreamType, EntityMetrics } from '../../../../common/entities/types'; +import type { EntityDataStreamType } from '../../../../common/entities/types'; export interface MergedServiceEntity { serviceName: string; agentName: AgentName; dataStreamTypes: EntityDataStreamType[]; environments: string[]; - metrics: EntityMetrics[]; - hasLogMetrics: boolean; } export function mergeEntities({ @@ -40,10 +37,6 @@ export function mergeEntities({ } function mergeFunc(entity: EntityLatestServiceRaw, existingEntity?: MergedServiceEntity) { - const hasLogMetrics = isFiniteNumber(entity.entity.metrics.logRate) - ? entity.entity.metrics.logRate > 0 - : false; - const commonEntityFields = { serviceName: entity.service.name, agentName: entity.agent.name[0], @@ -55,8 +48,6 @@ function mergeFunc(entity: EntityLatestServiceRaw, existingEntity?: MergedServic ...commonEntityFields, dataStreamTypes: entity.source_data_stream.type, environments: compact([entity?.service.environment]), - metrics: [entity.entity.metrics], - hasLogMetrics, }; } return { @@ -65,7 +56,5 @@ function mergeFunc(entity: EntityLatestServiceRaw, existingEntity?: MergedServic compact([...(existingEntity?.dataStreamTypes ?? []), ...entity.source_data_stream.type]) ), environments: uniq(compact([...existingEntity?.environments, entity?.service.environment])), - metrics: [...existingEntity?.metrics, entity.entity.metrics], - hasLogMetrics: hasLogMetrics || existingEntity.hasLogMetrics, }; } diff --git a/x-pack/plugins/observability_solution/infra/common/ui_settings.ts b/x-pack/plugins/observability_solution/infra/common/ui_settings.ts new file mode 100644 index 0000000000000..95f1ee0a44bae --- /dev/null +++ b/x-pack/plugins/observability_solution/infra/common/ui_settings.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. + */ + +/** + * uiSettings definitions for the logs_data_access plugin. + */ +import { schema } from '@kbn/config-schema'; +import { UiSettingsParams } from '@kbn/core-ui-settings-common'; +import { i18n } from '@kbn/i18n'; +import { OBSERVABILITY_ENABLE_LOGS_STREAM } from '@kbn/management-settings-ids'; + +export const uiSettings: Record = { + [OBSERVABILITY_ENABLE_LOGS_STREAM]: { + category: ['observability'], + name: i18n.translate('xpack.infra.enableLogsStream', { + defaultMessage: 'Logs Stream', + }), + value: false, + description: i18n.translate('xpack.infra.enableLogsStreamDescription', { + defaultMessage: 'Enables the legacy Logs Stream application and dashboard panel. ', + }), + type: 'boolean', + schema: schema.boolean(), + requiresPageReload: true, + }, +}; diff --git a/x-pack/plugins/observability_solution/infra/public/components/log_stream/log_stream_react_embeddable.tsx b/x-pack/plugins/observability_solution/infra/public/components/log_stream/log_stream_react_embeddable.tsx index bbb2e09d8660a..1193b81379219 100644 --- a/x-pack/plugins/observability_solution/infra/public/components/log_stream/log_stream_react_embeddable.tsx +++ b/x-pack/plugins/observability_solution/infra/public/components/log_stream/log_stream_react_embeddable.tsx @@ -6,6 +6,8 @@ */ import React, { FC, PropsWithChildren, useEffect, useMemo, useState } from 'react'; +import { i18n } from '@kbn/i18n'; +import { EuiCallOut, EuiLink } from '@elastic/eui'; import { ReactEmbeddableFactory } from '@kbn/embeddable-plugin/public'; import { initializeTimeRange, @@ -17,6 +19,9 @@ import { AppMountParameters, CoreStart } from '@kbn/core/public'; import { EuiThemeProvider } from '@kbn/kibana-react-plugin/common'; import { Query } from '@kbn/es-query'; import { KibanaRenderContextProvider } from '@kbn/react-kibana-context-render'; +import { euiThemeVars } from '@kbn/ui-theme'; +import useLocalStorage from 'react-use/lib/useLocalStorage'; +import { FormattedMessage } from '@kbn/i18n-react'; import type { LogStreamApi, LogStreamSerializedState, Services } from './types'; import { datemathToEpochMillis } from '../../utils/datemath'; import { LOG_STREAM_EMBEDDABLE } from './constants'; @@ -81,7 +86,7 @@ export function getLogStreamEmbeddableFactory(services: Services) { theme$={services.coreStart.theme.theme$} > -
    +
    +
    @@ -101,6 +107,53 @@ export function getLogStreamEmbeddableFactory(services: Services) { return factory; } +const DISMISSAL_STORAGE_KEY = 'observability:logStreamEmbeddableDeprecationCalloutDismissed'; +const SAVED_SEARCH_DOCS_URL = + 'https://www.elastic.co/guide/en/kibana/current/save-open-search.html'; + +const DeprecationCallout = () => { + const [isDismissed, setDismissed] = useLocalStorage(DISMISSAL_STORAGE_KEY, false); + + if (isDismissed) { + return null; + } + + return ( + setDismissed(true)} + css={{ + position: 'absolute', + bottom: euiThemeVars.euiSizeM, + right: euiThemeVars.euiSizeM, + width: 'min(100%, 40ch)', + }} + > +

    + + {i18n.translate( + 'xpack.infra.logsStreamEmbeddable.deprecationWarningDescription.savedSearchesLinkLabel', + { defaultMessage: 'saved searches' } + )} + + ), + }} + /> +

    +
    + ); +}; + export interface LogStreamEmbeddableProvidersProps { core: CoreStart; pluginStart: InfraClientStartExports; diff --git a/x-pack/plugins/observability_solution/infra/public/components/logging/log_analysis_setup/missing_results_privileges_prompt.tsx b/x-pack/plugins/observability_solution/infra/public/components/logging/log_analysis_setup/missing_results_privileges_prompt.tsx index 97eeeabe8721b..dce819ffb0930 100644 --- a/x-pack/plugins/observability_solution/infra/public/components/logging/log_analysis_setup/missing_results_privileges_prompt.tsx +++ b/x-pack/plugins/observability_solution/infra/public/components/logging/log_analysis_setup/missing_results_privileges_prompt.tsx @@ -16,6 +16,7 @@ import { UserManagementLink } from './user_management_link'; export const MissingResultsPrivilegesPrompt: React.FunctionComponent = () => ( {missingMlPrivilegesTitle}} body={

    {missingMlResultsPrivilegesDescription}

    } actions={} diff --git a/x-pack/plugins/observability_solution/infra/public/components/logging/log_analysis_setup/missing_setup_privileges_prompt.tsx b/x-pack/plugins/observability_solution/infra/public/components/logging/log_analysis_setup/missing_setup_privileges_prompt.tsx index f959c5035d1a4..4e2a360b55ceb 100644 --- a/x-pack/plugins/observability_solution/infra/public/components/logging/log_analysis_setup/missing_setup_privileges_prompt.tsx +++ b/x-pack/plugins/observability_solution/infra/public/components/logging/log_analysis_setup/missing_setup_privileges_prompt.tsx @@ -16,6 +16,7 @@ import { UserManagementLink } from './user_management_link'; export const MissingSetupPrivilegesPrompt: React.FunctionComponent = () => ( {missingMlPrivilegesTitle}} body={

    {missingMlSetupPrivilegesDescription}

    } actions={} diff --git a/x-pack/plugins/observability_solution/infra/public/components/logs_deprecation_callout.tsx b/x-pack/plugins/observability_solution/infra/public/components/logs_deprecation_callout.tsx index 71ae9698ea3b9..63107f4a4d031 100644 --- a/x-pack/plugins/observability_solution/infra/public/components/logs_deprecation_callout.tsx +++ b/x-pack/plugins/observability_solution/infra/public/components/logs_deprecation_callout.tsx @@ -18,14 +18,38 @@ import { css } from '@emotion/css'; import { SharePublicStart } from '@kbn/share-plugin/public/plugin'; import { useKibanaContextForPlugin } from '../hooks/use_kibana'; -const DISMISSAL_STORAGE_KEY = 'log_stream_deprecation_callout_dismissed'; +const pageConfigurations = { + stream: { + dismissalStorageKey: 'log_stream_deprecation_callout_dismissed', + message: i18n.translate('xpack.infra.logsDeprecationCallout.p.theNewLogsExplorerLabel', { + defaultMessage: + 'The new Logs Explorer makes viewing and inspecting your logs easier with more features, better performance, and more intuitive navigation. We recommend switching to Logs Explorer, as it will replace Logs Stream in a future version.', + }), + }, + settings: { + dismissalStorageKey: 'log_settings_deprecation_callout_dismissed', + message: i18n.translate( + 'xpack.infra.logsSettingsDeprecationCallout.p.theNewLogsExplorerLabel', + { + defaultMessage: + 'These settings only apply to the legacy Logs Stream app, and we do not recommend configuring them. Instead, use Logs Explorer which makes viewing and inspecting your logs easier with more features, better performance, and more intuitive navigation.', + } + ), + }, +}; + +interface LogsDeprecationCalloutProps { + page: keyof typeof pageConfigurations; +} -export const LogsDeprecationCallout = () => { +export const LogsDeprecationCallout = ({ page }: LogsDeprecationCalloutProps) => { const { services: { share }, } = useKibanaContextForPlugin(); - const [isDismissed, setDismissed] = useLocalStorage(DISMISSAL_STORAGE_KEY, false); + const { dismissalStorageKey, message } = pageConfigurations[page]; + + const [isDismissed, setDismissed] = useLocalStorage(dismissalStorageKey, false); if (isDismissed) { return null; @@ -42,12 +66,7 @@ export const LogsDeprecationCallout = () => { onDismiss={() => setDismissed(true)} className={calloutStyle} > -

    - {i18n.translate('xpack.infra.logsDeprecationCallout.p.theNewLogsExplorerLabel', { - defaultMessage: - 'The new Logs Explorer makes viewing and inspecting your logs easier with more features, better performance, and more intuitive navigation. We recommend switching to Logs Explorer, as it will replace Logs Stream in a future version.', - })} -

    +

    {message}

    void; - -export type DispatchWithOptionalAction = (_arg?: Type | unknown) => void; - -export interface UseBooleanHandlers { - on: VoidHandler; - off: VoidHandler; - toggle: DispatchWithOptionalAction; -} - -export type UseBooleanResult = [boolean, UseBooleanHandlers]; - -export const useBoolean = (initialValue: boolean = false): UseBooleanResult => { - const [value, toggle] = useToggle(initialValue); - - const handlers = useMemo( - () => ({ - toggle, - on: () => toggle(true), - off: () => toggle(false), - }), - [toggle] - ); - - return [value, handlers]; -}; diff --git a/x-pack/plugins/observability_solution/infra/public/observability_logs/log_stream_page/state/src/selectors.ts b/x-pack/plugins/observability_solution/infra/public/observability_logs/log_stream_page/state/src/selectors.ts index c6bfafd020ab2..6f00ce32097e9 100644 --- a/x-pack/plugins/observability_solution/infra/public/observability_logs/log_stream_page/state/src/selectors.ts +++ b/x-pack/plugins/observability_solution/infra/public/observability_logs/log_stream_page/state/src/selectors.ts @@ -5,8 +5,8 @@ * 2.0. */ +import { MatchedStateFromActor } from '@kbn/xstate-utils'; import { LogStreamQueryActorRef } from '../../../log_stream_query_state'; -import { MatchedStateFromActor } from '../../../xstate_helpers'; import { LogStreamPageActorRef } from './state_machine'; type LogStreamPageStateWithLogViewIndices = diff --git a/x-pack/plugins/observability_solution/infra/public/observability_logs/log_stream_page/state/src/state_machine.ts b/x-pack/plugins/observability_solution/infra/public/observability_logs/log_stream_page/state/src/state_machine.ts index d2a71c65702eb..e2755b29d21e3 100644 --- a/x-pack/plugins/observability_solution/infra/public/observability_logs/log_stream_page/state/src/state_machine.ts +++ b/x-pack/plugins/observability_solution/infra/public/observability_logs/log_stream_page/state/src/state_machine.ts @@ -10,6 +10,7 @@ import { TimeRange } from '@kbn/es-query'; import { actions, ActorRefFrom, createMachine, EmittedFrom } from 'xstate'; import { DEFAULT_REFRESH_INTERVAL } from '@kbn/logs-shared-plugin/common'; import type { LogViewNotificationChannel } from '@kbn/logs-shared-plugin/public'; +import { OmitDeprecatedState } from '@kbn/xstate-utils'; import { datemathToEpochMillis } from '../../../../utils/datemath'; import { createLogStreamPositionStateMachine } from '../../../log_stream_position_state/src/state_machine'; import { @@ -17,7 +18,6 @@ import { DEFAULT_TIMERANGE, LogStreamQueryStateMachineDependencies, } from '../../../log_stream_query_state'; -import { OmitDeprecatedState } from '../../../xstate_helpers'; import { waitForInitialQueryParameters, waitForInitialPositionParameters, diff --git a/x-pack/plugins/observability_solution/infra/public/observability_logs/log_stream_position_state/src/state_machine.ts b/x-pack/plugins/observability_solution/infra/public/observability_logs/log_stream_position_state/src/state_machine.ts index 868f29a5c07e9..0cc26d3e6ed35 100644 --- a/x-pack/plugins/observability_solution/infra/public/observability_logs/log_stream_position_state/src/state_machine.ts +++ b/x-pack/plugins/observability_solution/infra/public/observability_logs/log_stream_position_state/src/state_machine.ts @@ -10,8 +10,8 @@ import { IKbnUrlStateStorage } from '@kbn/kibana-utils-plugin/public'; import { convertISODateToNanoPrecision } from '@kbn/logs-shared-plugin/common'; import moment from 'moment'; import { actions, ActorRefFrom, createMachine, EmittedFrom, SpecialTargets } from 'xstate'; +import { OmitDeprecatedState, sendIfDefined } from '@kbn/xstate-utils'; import { isSameTimeKey } from '../../../../common/time'; -import { OmitDeprecatedState, sendIfDefined } from '../../xstate_helpers'; import { DESIRED_BUFFER_PAGES, RELATIVE_END_UPDATE_DELAY } from './defaults'; import { LogStreamPositionNotificationEventSelectors } from './notifications'; import type { diff --git a/x-pack/plugins/observability_solution/infra/public/observability_logs/log_stream_query_state/src/state_machine.ts b/x-pack/plugins/observability_solution/infra/public/observability_logs/log_stream_query_state/src/state_machine.ts index 1c0de464121c8..5570faf16f3f8 100644 --- a/x-pack/plugins/observability_solution/infra/public/observability_logs/log_stream_query_state/src/state_machine.ts +++ b/x-pack/plugins/observability_solution/infra/public/observability_logs/log_stream_query_state/src/state_machine.ts @@ -15,7 +15,7 @@ import { EsQueryConfig } from '@kbn/es-query'; import { IKbnUrlStateStorage } from '@kbn/kibana-utils-plugin/public'; import { actions, ActorRefFrom, createMachine, SpecialTargets, send } from 'xstate'; import { DEFAULT_REFRESH_INTERVAL } from '@kbn/logs-shared-plugin/common'; -import { OmitDeprecatedState, sendIfDefined } from '../../xstate_helpers'; +import { OmitDeprecatedState, sendIfDefined } from '@kbn/xstate-utils'; import { logStreamQueryNotificationEventSelectors } from './notifications'; import { subscribeToFilterSearchBarChanges, diff --git a/x-pack/plugins/observability_solution/infra/public/observability_logs/xstate_helpers/src/index.ts b/x-pack/plugins/observability_solution/infra/public/observability_logs/xstate_helpers/src/index.ts index 8e6f993a9755e..67b23e66b78e8 100644 --- a/x-pack/plugins/observability_solution/infra/public/observability_logs/xstate_helpers/src/index.ts +++ b/x-pack/plugins/observability_solution/infra/public/observability_logs/xstate_helpers/src/index.ts @@ -6,7 +6,4 @@ */ export * from './invalid_state_callout'; -export * from './notification_channel'; -export * from './send_actions'; -export * from './types'; export * from './state_machine_playground'; diff --git a/x-pack/plugins/observability_solution/infra/public/observability_logs/xstate_helpers/src/notification_channel.ts b/x-pack/plugins/observability_solution/infra/public/observability_logs/xstate_helpers/src/notification_channel.ts deleted file mode 100644 index 0108ab0225176..0000000000000 --- a/x-pack/plugins/observability_solution/infra/public/observability_logs/xstate_helpers/src/notification_channel.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; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { ReplaySubject } from 'rxjs'; -import { ActionFunction, EventObject, Expr, Subscribable } from 'xstate'; - -export interface NotificationChannel { - createService: () => Subscribable; - notify: ( - eventExpr: Expr - ) => ActionFunction; -} - -export const createNotificationChannel = < - TContext, - TEvent extends EventObject, - TSentEvent ->(): NotificationChannel => { - const eventsSubject = new ReplaySubject(1); - - const createService = () => eventsSubject.asObservable(); - - const notify = - (eventExpr: Expr) => - (context: TContext, event: TEvent) => { - const eventToSend = eventExpr(context, event); - - if (eventToSend != null) { - eventsSubject.next(eventToSend); - } - }; - - return { - createService, - notify, - }; -}; diff --git a/x-pack/plugins/observability_solution/infra/public/observability_logs/xstate_helpers/src/send_actions.test.ts b/x-pack/plugins/observability_solution/infra/public/observability_logs/xstate_helpers/src/send_actions.test.ts deleted file mode 100644 index cf446fec63b3f..0000000000000 --- a/x-pack/plugins/observability_solution/infra/public/observability_logs/xstate_helpers/src/send_actions.test.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 { actions, ActorRef, EventObject } from 'xstate'; -import { sendIfDefined } from './send_actions'; - -describe('function sendIfDefined', () => { - it('sends the events to the specified target', () => { - const actor = createMockActor(); - const createEvent = (context: {}) => ({ - type: 'testEvent', - }); - - const action = sendIfDefined(actor)(createEvent).get({}, { type: 'triggeringEvent' }); - - expect(action).toEqual([ - actions.send('testEvent', { - to: actor, - }), - ]); - }); - - it('sends the events created by the event expression', () => { - const actor = createMockActor(); - const createEvent = (context: {}) => ({ - type: 'testEvent', - payload: 'payload', - }); - - const action = sendIfDefined(actor)(createEvent).get({}, { type: 'triggeringEvent' }); - - expect(action).toEqual([ - actions.send( - { - type: 'testEvent', - payload: 'payload', - }, - { - to: actor, - } - ), - ]); - }); - - it("doesn't send anything when the event expression returns undefined", () => { - const actor = createMockActor(); - const createEvent = (context: {}) => undefined; - - const action = sendIfDefined(actor)(createEvent).get({}, { type: 'triggeringEvent' }); - - expect(action).toEqual(undefined); - }); -}); - -const createMockActor = (): ActorRef => ({ - getSnapshot: jest.fn(), - id: 'mockActor', - send: jest.fn(), - subscribe: jest.fn(), - [Symbol.observable]() { - return this; - }, -}); diff --git a/x-pack/plugins/observability_solution/infra/public/observability_logs/xstate_helpers/src/send_actions.ts b/x-pack/plugins/observability_solution/infra/public/observability_logs/xstate_helpers/src/send_actions.ts deleted file mode 100644 index 375fd831b030f..0000000000000 --- a/x-pack/plugins/observability_solution/infra/public/observability_logs/xstate_helpers/src/send_actions.ts +++ /dev/null @@ -1,36 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { - actions, - ActorRef, - AnyEventObject, - EventObject, - Expr, - PureAction, - SendActionOptions, -} from 'xstate'; - -export const sendIfDefined = - (target: string | ActorRef) => - ( - eventExpr: Expr, - options?: SendActionOptions - ): PureAction => { - return actions.pure((context, event) => { - const targetEvent = eventExpr(context, event); - - return targetEvent != null - ? [ - actions.send(targetEvent, { - ...options, - to: target, - }), - ] - : undefined; - }); - }; diff --git a/x-pack/plugins/observability_solution/infra/public/observability_logs/xstate_helpers/src/types.ts b/x-pack/plugins/observability_solution/infra/public/observability_logs/xstate_helpers/src/types.ts deleted file mode 100644 index 05e75c5fe6e45..0000000000000 --- a/x-pack/plugins/observability_solution/infra/public/observability_logs/xstate_helpers/src/types.ts +++ /dev/null @@ -1,43 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import type { ActorRef, ActorRefWithDeprecatedState, EmittedFrom, State, StateValue } from 'xstate'; - -export type OmitDeprecatedState> = Omit< - T, - 'state' ->; - -export type MatchedState< - TState extends State, - TStateValue extends StateValue -> = TState extends State< - any, - infer TEvent, - infer TStateSchema, - infer TTypestate, - infer TResolvedTypesMeta -> - ? State< - (TTypestate extends any - ? { value: TStateValue; context: any } extends TTypestate - ? TTypestate - : never - : never)['context'], - TEvent, - TStateSchema, - TTypestate, - TResolvedTypesMeta - > & { - value: TStateValue; - } - : never; - -export type MatchedStateFromActor< - TActorRef extends ActorRef, - TStateValue extends StateValue -> = MatchedState, TStateValue>; diff --git a/x-pack/plugins/observability_solution/infra/public/pages/logs/log_entry_categories/page.tsx b/x-pack/plugins/observability_solution/infra/public/pages/logs/log_entry_categories/page.tsx index f5b1e89c69e0b..650a5b119d755 100644 --- a/x-pack/plugins/observability_solution/infra/public/pages/logs/log_entry_categories/page.tsx +++ b/x-pack/plugins/observability_solution/infra/public/pages/logs/log_entry_categories/page.tsx @@ -7,8 +7,11 @@ import { EuiErrorBoundary } from '@elastic/eui'; import React from 'react'; +import { MissingResultsPrivilegesPrompt } from '../../../components/logging/log_analysis_setup'; +import { useLogAnalysisCapabilitiesContext } from '../../../containers/logs/log_analysis'; +import { SubscriptionSplashPage } from '../../../components/subscription_splash_content'; import { useLogsBreadcrumbs } from '../../../hooks/use_logs_breadcrumbs'; -import { LogEntryCategoriesPageContent } from './page_content'; +import { CategoriesPageTemplate, LogEntryCategoriesPageContent } from './page_content'; import { LogEntryCategoriesPageProviders } from './page_providers'; import { logCategoriesTitle } from '../../../translations'; import { LogMlJobIdFormatsShimProvider } from '../shared/use_log_ml_job_id_formats_shim'; @@ -20,6 +23,28 @@ export const LogEntryCategoriesPage = () => { }, ]); + const { hasLogAnalysisReadCapabilities, hasLogAnalysisCapabilites } = + useLogAnalysisCapabilitiesContext(); + + if (!hasLogAnalysisCapabilites) { + return ( + + ); + } + + if (!hasLogAnalysisReadCapabilities) { + return ( + + + + ); + } + return ( diff --git a/x-pack/plugins/observability_solution/infra/public/pages/logs/log_entry_categories/page_content.tsx b/x-pack/plugins/observability_solution/infra/public/pages/logs/log_entry_categories/page_content.tsx index c58ffc5f36e84..8059cdcb093e2 100644 --- a/x-pack/plugins/observability_solution/infra/public/pages/logs/log_entry_categories/page_content.tsx +++ b/x-pack/plugins/observability_solution/infra/public/pages/logs/log_entry_categories/page_content.tsx @@ -13,14 +13,12 @@ import { isJobStatusWithResults, logEntryCategoriesJobType } from '../../../../c import { LoadingPage } from '../../../components/loading_page'; import { LogAnalysisSetupStatusUnknownPrompt, - MissingResultsPrivilegesPrompt, MissingSetupPrivilegesPrompt, } from '../../../components/logging/log_analysis_setup'; import { LogAnalysisSetupFlyout, useLogAnalysisSetupFlyoutStateContext, } from '../../../components/logging/log_analysis_setup/setup_flyout'; -import { SubscriptionSplashPage } from '../../../components/subscription_splash_content'; import { useLogAnalysisCapabilitiesContext } from '../../../containers/logs/log_analysis'; import { useLogEntryCategoriesModuleContext } from '../../../containers/logs/log_analysis/modules/log_entry_categories'; import { LogsPageTemplate } from '../shared/page_template'; @@ -33,11 +31,8 @@ const logCategoriesTitle = i18n.translate('xpack.infra.logs.logCategoriesTitle', }); export const LogEntryCategoriesPageContent = () => { - const { - hasLogAnalysisCapabilites, - hasLogAnalysisReadCapabilities, - hasLogAnalysisSetupCapabilities, - } = useLogAnalysisCapabilitiesContext(); + const { hasLogAnalysisReadCapabilities, hasLogAnalysisSetupCapabilities } = + useLogAnalysisCapabilitiesContext(); const { fetchJobStatus, setupStatus, jobStatus } = useLogEntryCategoriesModuleContext(); @@ -55,22 +50,7 @@ export const LogEntryCategoriesPageContent = () => { const { idFormats } = useLogMlJobIdFormatsShimContext(); - if (!hasLogAnalysisCapabilites) { - return ( - - ); - } else if (!hasLogAnalysisReadCapabilities) { - return ( - - - - ); - } else if (setupStatus.type === 'initializing') { + if (setupStatus.type === 'initializing') { return ( { const allowedSetupModules = ['logs_ui_categories' as const]; -const CategoriesPageTemplate: React.FC = ({ +export const CategoriesPageTemplate: React.FC = ({ children, ...rest }) => { diff --git a/x-pack/plugins/observability_solution/infra/public/pages/logs/log_entry_rate/page.tsx b/x-pack/plugins/observability_solution/infra/public/pages/logs/log_entry_rate/page.tsx index 50a4852c458c4..ed46ea9dc2680 100644 --- a/x-pack/plugins/observability_solution/infra/public/pages/logs/log_entry_rate/page.tsx +++ b/x-pack/plugins/observability_solution/infra/public/pages/logs/log_entry_rate/page.tsx @@ -7,18 +7,44 @@ import { EuiErrorBoundary } from '@elastic/eui'; import React from 'react'; -import { LogEntryRatePageContent } from './page_content'; +import { SubscriptionSplashPage } from '../../../components/subscription_splash_content'; +import { MissingResultsPrivilegesPrompt } from '../../../components/logging/log_analysis_setup'; +import { useLogAnalysisCapabilitiesContext } from '../../../containers/logs/log_analysis'; +import { AnomaliesPageTemplate, LogEntryRatePageContent } from './page_content'; import { LogEntryRatePageProviders } from './page_providers'; import { useLogsBreadcrumbs } from '../../../hooks/use_logs_breadcrumbs'; -import { anomaliesTitle } from '../../../translations'; +import { logsAnomaliesTitle } from '../../../translations'; import { LogMlJobIdFormatsShimProvider } from '../shared/use_log_ml_job_id_formats_shim'; export const LogEntryRatePage = () => { useLogsBreadcrumbs([ { - text: anomaliesTitle, + text: logsAnomaliesTitle, }, ]); + + const { hasLogAnalysisReadCapabilities, hasLogAnalysisCapabilites } = + useLogAnalysisCapabilitiesContext(); + + if (!hasLogAnalysisCapabilites) { + return ( + + ); + } + + if (!hasLogAnalysisReadCapabilities) { + return ( + + + + ); + } + return ( diff --git a/x-pack/plugins/observability_solution/infra/public/pages/logs/log_entry_rate/page_content.tsx b/x-pack/plugins/observability_solution/infra/public/pages/logs/log_entry_rate/page_content.tsx index e4dc0694c3f75..350094b5df6a3 100644 --- a/x-pack/plugins/observability_solution/infra/public/pages/logs/log_entry_rate/page_content.tsx +++ b/x-pack/plugins/observability_solution/infra/public/pages/logs/log_entry_rate/page_content.tsx @@ -18,14 +18,12 @@ import { import { LoadingPage } from '../../../components/loading_page'; import { LogAnalysisSetupStatusUnknownPrompt, - MissingResultsPrivilegesPrompt, MissingSetupPrivilegesPrompt, } from '../../../components/logging/log_analysis_setup'; import { LogAnalysisSetupFlyout, useLogAnalysisSetupFlyoutStateContext, } from '../../../components/logging/log_analysis_setup/setup_flyout'; -import { SubscriptionSplashPage } from '../../../components/subscription_splash_content'; import { useLogAnalysisCapabilitiesContext } from '../../../containers/logs/log_analysis'; import { useLogEntryCategoriesModuleContext } from '../../../containers/logs/log_analysis/modules/log_entry_categories'; import { useLogEntryRateModuleContext } from '../../../containers/logs/log_analysis/modules/log_entry_rate'; @@ -36,16 +34,13 @@ import { useLogMlJobIdFormatsShimContext } from '../shared/use_log_ml_job_id_for const JOB_STATUS_POLLING_INTERVAL = 30000; -const anomaliesTitle = i18n.translate('xpack.infra.logs.anomaliesPageTitle', { +const logsAnomaliesTitle = i18n.translate('xpack.infra.logs.anomaliesPageTitle', { defaultMessage: 'Anomalies', }); export const LogEntryRatePageContent = memo(() => { - const { - hasLogAnalysisCapabilites, - hasLogAnalysisReadCapabilities, - hasLogAnalysisSetupCapabilities, - } = useLogAnalysisCapabilitiesContext(); + const { hasLogAnalysisReadCapabilities, hasLogAnalysisSetupCapabilities } = + useLogAnalysisCapabilitiesContext(); const { fetchJobStatus: fetchLogEntryCategoriesJobStatus, @@ -96,22 +91,7 @@ export const LogEntryRatePageContent = memo(() => { const { idFormats } = useLogMlJobIdFormatsShimContext(); - if (!hasLogAnalysisCapabilites) { - return ( - - ); - } else if (!hasLogAnalysisReadCapabilities) { - return ( - - - - ); - } else if ( + if ( logEntryCategoriesSetupStatus.type === 'initializing' || logEntryRateSetupStatus.type === 'initializing' ) { @@ -137,7 +117,7 @@ export const LogEntryRatePageContent = memo(() => { ) { return ( <> - + ); @@ -159,7 +139,7 @@ export const LogEntryRatePageContent = memo(() => { } }); -const AnomaliesPageTemplate: React.FC = ({ +export const AnomaliesPageTemplate: React.FC = ({ children, ...rest }) => { @@ -172,7 +152,7 @@ const AnomaliesPageTemplate: React.FC = ({ rest.isEmptyState ? undefined : { - pageTitle: anomaliesTitle, + pageTitle: logsAnomaliesTitle, } } {...rest} diff --git a/x-pack/plugins/observability_solution/infra/public/pages/logs/page_content.tsx b/x-pack/plugins/observability_solution/infra/public/pages/logs/page_content.tsx index 5b5965bb2d5ec..ecf5af5572b31 100644 --- a/x-pack/plugins/observability_solution/infra/public/pages/logs/page_content.tsx +++ b/x-pack/plugins/observability_solution/infra/public/pages/logs/page_content.tsx @@ -9,26 +9,36 @@ import { EuiFlexGroup, EuiFlexItem, EuiHeaderLink, EuiHeaderLinks } from '@elast import { i18n } from '@kbn/i18n'; import React, { useContext } from 'react'; import { Routes, Route } from '@kbn/shared-ux-router'; -import { useKibana } from '@kbn/kibana-react-plugin/public'; +import { useKibana, useUiSetting } from '@kbn/kibana-react-plugin/public'; import { HeaderMenuPortal, useLinkProps } from '@kbn/observability-shared-plugin/public'; import { SharePublicStart } from '@kbn/share-plugin/public/plugin'; import { ObservabilityOnboardingLocatorParams, OBSERVABILITY_ONBOARDING_LOCATOR, + AllDatasetsLocatorParams, + ALL_DATASETS_LOCATOR_ID, } from '@kbn/deeplinks-observability'; import { dynamic } from '@kbn/shared-ux-utility'; +import { isDevMode } from '@kbn/xstate-utils'; +import { OBSERVABILITY_ENABLE_LOGS_STREAM } from '@kbn/management-settings-ids'; import { LazyAlertDropdownWrapper } from '../../alerting/log_threshold'; import { HelpCenterContent } from '../../components/help_center_content'; import { useReadOnlyBadge } from '../../hooks/use_readonly_badge'; import { HeaderActionMenuContext } from '../../containers/header_action_menu_provider'; import { RedirectWithQueryParams } from '../../utils/redirect_with_query_params'; -import { LogEntryCategoriesPage } from './log_entry_categories'; -import { LogEntryRatePage } from './log_entry_rate'; -import { LogsSettingsPage } from './settings'; -import { StreamPage } from './stream'; -import { isDevMode } from '../../utils/dev_mode'; import { NotFoundPage } from '../404'; +import { getLogsAppRoutes } from './routes'; +const StreamPage = dynamic(() => import('./stream').then((mod) => ({ default: mod.StreamPage }))); +const LogEntryCategoriesPage = dynamic(() => + import('./log_entry_categories').then((mod) => ({ default: mod.LogEntryCategoriesPage })) +); +const LogEntryRatePage = dynamic(() => + import('./log_entry_rate').then((mod) => ({ default: mod.LogEntryRatePage })) +); +const LogsSettingsPage = dynamic(() => + import('./settings').then((mod) => ({ default: mod.LogsSettingsPage })) +); const StateMachinePlayground = dynamic(() => import('../../observability_logs/xstate_helpers').then((mod) => ({ default: mod.StateMachinePlayground, @@ -37,6 +47,9 @@ const StateMachinePlayground = dynamic(() => export const LogsPageContent: React.FunctionComponent = () => { const { application, share } = useKibana<{ share: SharePublicStart }>().services; + + const isLogsStreamEnabled: boolean = useUiSetting(OBSERVABILITY_ENABLE_LOGS_STREAM, false); + const uiCapabilities = application?.capabilities; const onboardingLocator = share?.url.locators.get( OBSERVABILITY_ONBOARDING_LOCATOR @@ -47,30 +60,7 @@ export const LogsPageContent: React.FunctionComponent = () => { useReadOnlyBadge(!uiCapabilities?.logs?.save); - // !! Need to be kept in sync with the deepLinks in x-pack/plugins/observability_solution/infra/public/plugin.ts - const streamTab = { - app: 'logs', - title: streamTabTitle, - pathname: '/stream', - }; - - const anomaliesTab = { - app: 'logs', - title: anomaliesTabTitle, - pathname: '/anomalies', - }; - - const logCategoriesTab = { - app: 'logs', - title: logCategoriesTabTitle, - pathname: '/log-categories', - }; - - const settingsTab = { - app: 'logs', - title: settingsTabTitle, - pathname: '/settings', - }; + const routes = getLogsAppRoutes({ isLogsStreamEnabled }); const settingsLinkProps = useLinkProps({ app: 'logs', @@ -104,25 +94,36 @@ export const LogsPageContent: React.FunctionComponent = () => { )} - - - - + {routes.stream ? ( + + ) : ( + { + share.url.locators + .get(ALL_DATASETS_LOCATOR_ID) + ?.navigate({}); + + return null; + }} + /> + )} + + + {enableDeveloperRoutes && ( )} - - - - ( - - )} + + + + + } /> ); @@ -132,18 +133,6 @@ const pageTitle = i18n.translate('xpack.infra.header.logsTitle', { defaultMessage: 'Logs', }); -const streamTabTitle = i18n.translate('xpack.infra.logs.index.streamTabTitle', { - defaultMessage: 'Stream', -}); - -const anomaliesTabTitle = i18n.translate('xpack.infra.logs.index.anomaliesTabTitle', { - defaultMessage: 'Anomalies', -}); - -const logCategoriesTabTitle = i18n.translate('xpack.infra.logs.index.logCategoriesBetaBadgeTitle', { - defaultMessage: 'Categories', -}); - const settingsTabTitle = i18n.translate('xpack.infra.logs.index.settingsTabTitle', { defaultMessage: 'Settings', }); diff --git a/x-pack/plugins/observability_solution/infra/public/pages/logs/routes.ts b/x-pack/plugins/observability_solution/infra/public/pages/logs/routes.ts new file mode 100644 index 0000000000000..a5c38672a8bed --- /dev/null +++ b/x-pack/plugins/observability_solution/infra/public/pages/logs/routes.ts @@ -0,0 +1,56 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { + logsAnomaliesTitle, + logCategoriesTitle, + settingsTitle, + streamTitle, +} from '../../translations'; + +export interface LogsRoute { + id: string; + title: string; + path: string; +} + +export interface LogsAppRoutes { + logsAnomalies: LogsRoute; + logsCategories: LogsRoute; + settings: LogsRoute; + stream?: LogsRoute; +} + +export const getLogsAppRoutes = ({ isLogsStreamEnabled }: { isLogsStreamEnabled: boolean }) => { + const routes: LogsAppRoutes = { + logsAnomalies: { + id: 'anomalies', + title: logsAnomaliesTitle, + path: '/anomalies', + }, + logsCategories: { + id: 'log-categories', + title: logCategoriesTitle, + path: '/log-categories', + }, + settings: { + id: 'settings', + title: settingsTitle, + path: '/settings', + }, + }; + + if (isLogsStreamEnabled) { + routes.stream = { + id: 'stream', + title: streamTitle, + path: '/stream', + }; + } + + return routes; +}; diff --git a/x-pack/plugins/observability_solution/infra/public/pages/logs/settings/source_configuration_settings.tsx b/x-pack/plugins/observability_solution/infra/public/pages/logs/settings/source_configuration_settings.tsx index 557fe1cfab314..d1df2a5820dd3 100644 --- a/x-pack/plugins/observability_solution/infra/public/pages/logs/settings/source_configuration_settings.tsx +++ b/x-pack/plugins/observability_solution/infra/public/pages/logs/settings/source_configuration_settings.tsx @@ -20,6 +20,7 @@ import { useKibana } from '@kbn/kibana-react-plugin/public'; import { Prompt } from '@kbn/observability-shared-plugin/public'; import { useTrackPageview } from '@kbn/observability-shared-plugin/public'; import { useLogViewContext } from '@kbn/logs-shared-plugin/public'; +import { LogsDeprecationCallout } from '../../../components/logs_deprecation_callout'; import { SourceLoadingPage } from '../../../components/source_loading_page'; import { useLogsBreadcrumbs } from '../../../hooks/use_logs_breadcrumbs'; import { settingsTitle } from '../../../translations'; @@ -98,6 +99,7 @@ export const LogsSettingsPage = () => { data-test-subj="sourceConfigurationContent" restrictWidth > + diff --git a/x-pack/plugins/observability_solution/infra/public/pages/logs/stream/page_logs_content.tsx b/x-pack/plugins/observability_solution/infra/public/pages/logs/stream/page_logs_content.tsx index d40014ed4468b..f59d3c1f03fbf 100644 --- a/x-pack/plugins/observability_solution/infra/public/pages/logs/stream/page_logs_content.tsx +++ b/x-pack/plugins/observability_solution/infra/public/pages/logs/stream/page_logs_content.tsx @@ -26,6 +26,7 @@ import { useSelector } from '@xstate/react'; import stringify from 'json-stable-stringify'; import React, { useCallback, useEffect, useMemo } from 'react'; import usePrevious from 'react-use/lib/usePrevious'; +import { MatchedStateFromActor } from '@kbn/xstate-utils'; import { LogsDeprecationCallout } from '../../../components/logs_deprecation_callout'; import { TimeKey } from '../../../../common/time'; import { AutoSizer } from '../../../components/auto_sizer'; @@ -45,7 +46,6 @@ import { useLogStreamPageStateContext, } from '../../../observability_logs/log_stream_page/state'; import { type ParsedQuery } from '../../../observability_logs/log_stream_query_state'; -import { MatchedStateFromActor } from '../../../observability_logs/xstate_helpers'; import { datemathToEpochMillis, isValidDatemath } from '../../../utils/datemath'; import { LogsToolbar } from './page_toolbar'; import { PageViewLogInContext } from './page_view_log_in_context'; @@ -234,7 +234,7 @@ export const StreamPageLogsContent = React.memo<{ return ( <> - + diff --git a/x-pack/plugins/observability_solution/infra/public/pages/logs/stream/page_providers.tsx b/x-pack/plugins/observability_solution/infra/public/pages/logs/stream/page_providers.tsx index a8fd0cecf448b..497329782d879 100644 --- a/x-pack/plugins/observability_solution/infra/public/pages/logs/stream/page_providers.tsx +++ b/x-pack/plugins/observability_solution/infra/public/pages/logs/stream/page_providers.tsx @@ -15,6 +15,7 @@ import { useLogStreamContext, useLogViewContext, } from '@kbn/logs-shared-plugin/public'; +import { MatchedStateFromActor } from '@kbn/xstate-utils'; import { LogStreamPageActorRef, LogStreamPageCallbacks, @@ -22,7 +23,6 @@ import { import { LogEntryFlyoutProvider } from '../../../containers/logs/log_flyout'; import { LogViewConfigurationProvider } from '../../../containers/logs/log_view_configuration'; import { ViewLogInContextProvider } from '../../../containers/logs/view_log_in_context'; -import { MatchedStateFromActor } from '../../../observability_logs/xstate_helpers'; const ViewLogInContext: FC> = ({ children }) => { const { startTimestamp, endTimestamp } = useLogPositionStateContext(); diff --git a/x-pack/plugins/observability_solution/infra/public/pages/metrics/hosts/components/tabs/logs/logs_tab_content.tsx b/x-pack/plugins/observability_solution/infra/public/pages/metrics/hosts/components/tabs/logs/logs_tab_content.tsx index 27344ccd1f108..68a5db6d4d484 100644 --- a/x-pack/plugins/observability_solution/infra/public/pages/metrics/hosts/components/tabs/logs/logs_tab_content.tsx +++ b/x-pack/plugins/observability_solution/infra/public/pages/metrics/hosts/components/tabs/logs/logs_tab_content.tsx @@ -5,21 +5,37 @@ * 2.0. */ -import React, { useMemo } from 'react'; +import { QueryDslQueryContainer } from '@elastic/elasticsearch/lib/api/types'; import { EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n-react'; import { LogStream } from '@kbn/logs-shared-plugin/public'; -import { i18n } from '@kbn/i18n'; +import React, { useMemo } from 'react'; import { InfraLoadingPanel } from '../../../../../../components/loading'; +import { useKibanaContextForPlugin } from '../../../../../../hooks/use_kibana'; +import { useLogViewReference } from '../../../../../../hooks/use_log_view_reference'; +import { buildCombinedAssetFilter } from '../../../../../../utils/filters/build'; import { useHostsViewContext } from '../../../hooks/use_hosts_view'; -import { useUnifiedSearchContext } from '../../../hooks/use_unified_search'; import { useLogsSearchUrlState } from '../../../hooks/use_logs_search_url_state'; +import { useUnifiedSearchContext } from '../../../hooks/use_unified_search'; import { LogsLinkToStream } from './logs_link_to_stream'; import { LogsSearchBar } from './logs_search_bar'; -import { buildCombinedAssetFilter } from '../../../../../../utils/filters/build'; -import { useLogViewReference } from '../../../../../../hooks/use_log_view_reference'; export const LogsTabContent = () => { + const { + services: { + logsShared: { LogsOverview }, + }, + } = useKibanaContextForPlugin(); + const isLogsOverviewEnabled = LogsOverview.useIsEnabled(); + if (isLogsOverviewEnabled) { + return ; + } else { + return ; + } +}; + +export const LogsTabLogStreamContent = () => { const [filterQuery] = useLogsSearchUrlState(); const { getDateRangeAsTimestamp } = useUnifiedSearchContext(); const { from, to } = useMemo(() => getDateRangeAsTimestamp(), [getDateRangeAsTimestamp]); @@ -53,22 +69,7 @@ export const LogsTabContent = () => { }, [filterQuery.query, hostNodes]); if (loading || logViewLoading || !logView) { - return ( - - - - } - /> - - - ); + return ; } return ( @@ -112,3 +113,53 @@ const createHostsFilterQueryParam = (hostNodes: string[]): string => { return hostsQueryParam; }; + +const LogsTabLogsOverviewContent = () => { + const { + services: { + logsShared: { LogsOverview }, + }, + } = useKibanaContextForPlugin(); + + const { parsedDateRange } = useUnifiedSearchContext(); + const timeRange = useMemo( + () => ({ start: parsedDateRange.from, end: parsedDateRange.to }), + [parsedDateRange.from, parsedDateRange.to] + ); + + const { hostNodes, loading, error } = useHostsViewContext(); + const logFilters = useMemo( + () => [ + buildCombinedAssetFilter({ + field: 'host.name', + values: hostNodes.map((p) => p.name), + }).query as QueryDslQueryContainer, + ], + [hostNodes] + ); + + if (loading) { + return ; + } else if (error != null) { + return ; + } else { + return ; + } +}; + +const LogsTabLoadingContent = () => ( + + + + } + /> + + +); diff --git a/x-pack/plugins/observability_solution/infra/public/pages/metrics/inventory_view/components/waffle/waffle_group_by_controls.tsx b/x-pack/plugins/observability_solution/infra/public/pages/metrics/inventory_view/components/waffle/waffle_group_by_controls.tsx index 47e5f58a258df..bca1a3858f5c9 100644 --- a/x-pack/plugins/observability_solution/infra/public/pages/metrics/inventory_view/components/waffle/waffle_group_by_controls.tsx +++ b/x-pack/plugins/observability_solution/infra/public/pages/metrics/inventory_view/components/waffle/waffle_group_by_controls.tsx @@ -17,7 +17,7 @@ import { FormattedMessage } from '@kbn/i18n-react'; import React from 'react'; import { InventoryItemType } from '@kbn/metrics-data-access-plugin/common'; import { css } from '@emotion/react'; -import { useBoolean } from '../../../../../hooks/use_boolean'; +import { useBoolean } from '@kbn/react-hooks'; import { InfraGroupByOptions } from '../../../../../common/inventory/types'; import { CustomFieldPanel } from './custom_field_panel'; import { SnapshotGroupBy } from '../../../../../../common/http_api/snapshot_api'; diff --git a/x-pack/plugins/observability_solution/infra/public/plugin.ts b/x-pack/plugins/observability_solution/infra/public/plugin.ts index 843a23bdfccc5..daaa3510e1660 100644 --- a/x-pack/plugins/observability_solution/infra/public/plugin.ts +++ b/x-pack/plugins/observability_solution/infra/public/plugin.ts @@ -33,6 +33,8 @@ import { type AssetDetailsLocatorParams, type InventoryLocatorParams, } from '@kbn/observability-shared-plugin/common'; +import { OBSERVABILITY_ENABLE_LOGS_STREAM } from '@kbn/management-settings-ids'; +import { NavigationEntry } from '@kbn/observability-shared-plugin/public'; import type { InfraPublicConfig } from '../common/plugin_config_types'; import { createInventoryMetricRuleType } from './alerting/inventory'; import { createLogThresholdRuleType } from './alerting/log_threshold'; @@ -53,7 +55,14 @@ import type { } from './types'; import { getLogsHasDataFetcher, getLogsOverviewDataFetcher } from './utils/logs_overview_fetchers'; import type { LogStreamSerializedState } from './components/log_stream/types'; -import { hostsTitle, inventoryTitle, metricsExplorerTitle, metricsTitle } from './translations'; +import { + hostsTitle, + inventoryTitle, + logsTitle, + metricsExplorerTitle, + metricsTitle, +} from './translations'; +import { LogsAppRoutes, LogsRoute, getLogsAppRoutes } from './pages/logs/routes'; export class Plugin implements InfraClientPluginClass { public config: InfraPublicConfig; @@ -77,6 +86,8 @@ export class Plugin implements InfraClientPluginClass { } setup(core: InfraClientCoreSetup, pluginsSetup: InfraClientSetupDeps) { + const isLogsStreamEnabled = core.uiSettings.get(OBSERVABILITY_ENABLE_LOGS_STREAM, false); + if (pluginsSetup.home) { registerFeatures(pluginsSetup.home); } @@ -125,6 +136,8 @@ export class Plugin implements InfraClientPluginClass { core.settings.client.get$(enableInfrastructureHostsView), ]); + const logRoutes = getLogsAppRoutes({ isLogsStreamEnabled }); + /** !! Need to be kept in sync with the deepLinks in x-pack/plugins/observability_solution/infra/public/plugin.ts */ pluginsSetup.observabilityShared.navigation.registerSections( startDep$AndHostViewFlag$.pipe( @@ -137,32 +150,18 @@ export class Plugin implements InfraClientPluginClass { ], isInfrastructureHostsViewEnabled, ]) => { - const { infrastructure, logs, discover, fleet } = capabilities; + const { infrastructure, logs } = capabilities; return [ ...(logs.show ? [ { - label: 'Logs', + label: logsTitle, sortKey: 200, - entries: [ - ...(discover?.show && fleet?.read - ? [ - { - label: 'Explorer', - app: 'observability-logs-explorer', - path: '/', - isBetaFeature: true, - }, - ] - : []), - ...(this.config.featureFlags.logsUIEnabled - ? [ - { label: 'Stream', app: 'logs', path: '/stream' }, - { label: 'Anomalies', app: 'logs', path: '/anomalies' }, - { label: 'Categories', app: 'logs', path: '/log-categories' }, - ] - : []), - ], + entries: getLogsNavigationEntries({ + capabilities, + config: this.config, + routes: logRoutes, + }), }, ] : []), @@ -230,37 +229,7 @@ export class Plugin implements InfraClientPluginClass { euiIconType: 'logoObservability', order: 8100, appRoute: '/app/logs', - // !! Need to be kept in sync with the routes in x-pack/plugins/observability_solution/infra/public/pages/logs/page_content.tsx - deepLinks: [ - { - id: 'stream', - title: i18n.translate('xpack.infra.logs.index.streamTabTitle', { - defaultMessage: 'Stream', - }), - path: '/stream', - }, - { - id: 'anomalies', - title: i18n.translate('xpack.infra.logs.index.anomaliesTabTitle', { - defaultMessage: 'Anomalies', - }), - path: '/anomalies', - }, - { - id: 'log-categories', - title: i18n.translate('xpack.infra.logs.index.logCategoriesBetaBadgeTitle', { - defaultMessage: 'Categories', - }), - path: '/log-categories', - }, - { - id: 'settings', - title: i18n.translate('xpack.infra.logs.index.settingsTabTitle', { - defaultMessage: 'Settings', - }), - path: '/settings', - }, - ], + deepLinks: Object.values(logRoutes), category: DEFAULT_APP_CATEGORIES.observability, mount: async (params: AppMountParameters) => { // mount callback should not use setup dependencies, get start dependencies instead @@ -384,44 +353,47 @@ export class Plugin implements InfraClientPluginClass { } start(core: InfraClientCoreStart, plugins: InfraClientStartDeps) { - const { http } = core; + const { http, uiSettings } = core; + const isLogsStreamEnabled = uiSettings.get(OBSERVABILITY_ENABLE_LOGS_STREAM, false); const inventoryViews = this.inventoryViews.start({ http }); const metricsExplorerViews = this.metricsExplorerViews?.start({ http }); const telemetry = this.telemetry.start(); - plugins.uiActions.registerAction({ - id: ADD_LOG_STREAM_ACTION_ID, - grouping: [COMMON_EMBEDDABLE_GROUPING.legacy], - order: 30, - getDisplayName: () => - i18n.translate('xpack.infra.logStreamEmbeddable.displayName', { - defaultMessage: 'Log stream (deprecated)', - }), - getDisplayNameTooltip: () => - i18n.translate('xpack.infra.logStreamEmbeddable.description', { - defaultMessage: - 'Add a table of live streaming logs. For a more efficient experience, we recommend using the Discover Page to create a saved search instead of using Log stream.', - }), - getIconType: () => 'logsApp', - isCompatible: async ({ embeddable }) => { - return apiCanAddNewPanel(embeddable); - }, - execute: async ({ embeddable }) => { - if (!apiCanAddNewPanel(embeddable)) throw new IncompatibleActionError(); - embeddable.addNewPanel( - { - panelType: LOG_STREAM_EMBEDDABLE, - initialState: { - title: i18n.translate('xpack.infra.logStreamEmbeddable.title', { - defaultMessage: 'Log stream', - }), + if (isLogsStreamEnabled) { + plugins.uiActions.registerAction({ + id: ADD_LOG_STREAM_ACTION_ID, + grouping: [COMMON_EMBEDDABLE_GROUPING.legacy], + order: 30, + getDisplayName: () => + i18n.translate('xpack.infra.logStreamEmbeddable.displayName', { + defaultMessage: 'Log stream (deprecated)', + }), + getDisplayNameTooltip: () => + i18n.translate('xpack.infra.logStreamEmbeddable.description', { + defaultMessage: + 'Add a table of live streaming logs. For a more efficient experience, we recommend using the Discover Page to create a saved search instead of using Log stream.', + }), + getIconType: () => 'logsApp', + isCompatible: async ({ embeddable }) => { + return apiCanAddNewPanel(embeddable); + }, + execute: async ({ embeddable }) => { + if (!apiCanAddNewPanel(embeddable)) throw new IncompatibleActionError(); + embeddable.addNewPanel( + { + panelType: LOG_STREAM_EMBEDDABLE, + initialState: { + title: i18n.translate('xpack.infra.logStreamEmbeddable.title', { + defaultMessage: 'Log stream', + }), + }, }, - }, - true - ); - }, - }); - plugins.uiActions.attachAction(ADD_PANEL_TRIGGER, ADD_LOG_STREAM_ACTION_ID); + true + ); + }, + }); + plugins.uiActions.attachAction(ADD_PANEL_TRIGGER, ADD_LOG_STREAM_ACTION_ID); + } const startContract: InfraClientStartExports = { inventoryViews, @@ -434,3 +406,42 @@ export class Plugin implements InfraClientPluginClass { stop() {} } + +const getLogsNavigationEntries = ({ + capabilities, + config, + routes, +}: { + capabilities: CoreStart['application']['capabilities']; + config: InfraPublicConfig; + routes: LogsAppRoutes; +}) => { + const entries: NavigationEntry[] = []; + + if (!config.featureFlags.logsUIEnabled) return entries; + + if (capabilities.discover?.show && capabilities.fleet?.read) { + entries.push({ + label: 'Explorer', + app: 'observability-logs-explorer', + path: '/', + isBetaFeature: true, + }); + } + + // Display Stream nav entry when Logs Stream is enabled + if (routes.stream) entries.push(createNavEntryFromRoute(routes.stream)); + // Display always Logs Anomalies and Logs Categories entries + entries.push(createNavEntryFromRoute(routes.logsAnomalies)); + entries.push(createNavEntryFromRoute(routes.logsCategories)); + // Display Logs Settings entry when Logs Stream is not enabled + if (!routes.stream) entries.push(createNavEntryFromRoute(routes.settings)); + + return entries; +}; + +const createNavEntryFromRoute = ({ path, title }: LogsRoute): NavigationEntry => ({ + app: 'logs', + label: title, + path, +}); diff --git a/x-pack/plugins/observability_solution/infra/public/translations.ts b/x-pack/plugins/observability_solution/infra/public/translations.ts index 2e9153ce171b9..ecb72b3df4b01 100644 --- a/x-pack/plugins/observability_solution/infra/public/translations.ts +++ b/x-pack/plugins/observability_solution/infra/public/translations.ts @@ -19,14 +19,14 @@ export const streamTitle = i18n.translate('xpack.infra.logs.index.streamTabTitle defaultMessage: 'Stream', }); -export const anomaliesTitle = i18n.translate('xpack.infra.logs.index.anomaliesTabTitle', { - defaultMessage: 'Anomalies', +export const logsAnomaliesTitle = i18n.translate('xpack.infra.logs.index.anomaliesTabTitle', { + defaultMessage: 'Logs Anomalies', }); export const logCategoriesTitle = i18n.translate( 'xpack.infra.logs.index.logCategoriesBetaBadgeTitle', { - defaultMessage: 'Categories', + defaultMessage: 'Logs Categories', } ); diff --git a/x-pack/plugins/observability_solution/infra/server/lib/adapters/framework/adapter_types.ts b/x-pack/plugins/observability_solution/infra/server/lib/adapters/framework/adapter_types.ts index 2cbf6b61623cf..b8dd11a17fb0b 100644 --- a/x-pack/plugins/observability_solution/infra/server/lib/adapters/framework/adapter_types.ts +++ b/x-pack/plugins/observability_solution/infra/server/lib/adapters/framework/adapter_types.ts @@ -39,6 +39,7 @@ import { ApmDataAccessPluginStart, } from '@kbn/apm-data-access-plugin/server'; import { LogsDataAccessPluginStart } from '@kbn/logs-data-access-plugin/server'; +import { ServerlessPluginStart } from '@kbn/serverless/server'; import type { EntityManagerServerPluginStart, EntityManagerServerPluginSetup, @@ -60,6 +61,7 @@ export interface InfraServerPluginSetupDeps { metricsDataAccess: MetricsDataPluginSetup; profilingDataAccess?: ProfilingDataAccessPluginSetup; apmDataAccess: ApmDataAccessPluginSetup; + serverless?: ServerlessPluginStart; entityManager: EntityManagerServerPluginSetup; } diff --git a/x-pack/plugins/observability_solution/infra/server/plugin.ts b/x-pack/plugins/observability_solution/infra/server/plugin.ts index 73d49ed938546..b8becb916a4e3 100644 --- a/x-pack/plugins/observability_solution/infra/server/plugin.ts +++ b/x-pack/plugins/observability_solution/infra/server/plugin.ts @@ -59,6 +59,7 @@ import { } from './types'; import { UsageCollector } from './usage/usage_collector'; import { mapSourceToLogView } from './utils/map_source_to_log_view'; +import { uiSettings } from '../common/ui_settings'; export const config: PluginConfigDescriptor = { schema: schema.object({ @@ -211,6 +212,9 @@ export class InfraServerPlugin const inventoryViews = this.inventoryViews.setup(); const metricsExplorerViews = this.metricsExplorerViews?.setup(); + // Register uiSettings config + core.uiSettings.register(uiSettings); + // Register saved object types core.savedObjects.registerType(infraSourceConfigurationSavedObjectType); core.savedObjects.registerType(inventoryViewSavedObjectType); diff --git a/x-pack/plugins/observability_solution/infra/tsconfig.json b/x-pack/plugins/observability_solution/infra/tsconfig.json index e7aade296fa8a..fea285b3a794e 100644 --- a/x-pack/plugins/observability_solution/infra/tsconfig.json +++ b/x-pack/plugins/observability_solution/infra/tsconfig.json @@ -110,6 +110,9 @@ "@kbn/observability-alerting-rule-utils", "@kbn/core-application-browser", "@kbn/shared-ux-page-no-data-types", + "@kbn/xstate-utils", + "@kbn/management-settings-ids", + "@kbn/core-ui-settings-common", "@kbn/entityManager-plugin", "@kbn/observability-utils", "@kbn/entities-schema" diff --git a/x-pack/plugins/observability_solution/inventory/.storybook/get_mock_inventory_context.tsx b/x-pack/plugins/observability_solution/inventory/.storybook/get_mock_inventory_context.tsx index 51aaeebc655f2..d90ce08aab1c6 100644 --- a/x-pack/plugins/observability_solution/inventory/.storybook/get_mock_inventory_context.tsx +++ b/x-pack/plugins/observability_solution/inventory/.storybook/get_mock_inventory_context.tsx @@ -13,6 +13,7 @@ import type { InferencePublicStart } from '@kbn/inference-plugin/public'; import type { ObservabilitySharedPluginStart } from '@kbn/observability-shared-plugin/public'; import type { UnifiedSearchPublicPluginStart } from '@kbn/unified-search-plugin/public'; import type { SharePluginStart } from '@kbn/share-plugin/public'; +import type { SpacesPluginStart } from '@kbn/spaces-plugin/public'; import type { InventoryKibanaContext } from '../public/hooks/use_kibana'; import type { ITelemetryClient } from '../public/services/telemetry/types'; @@ -33,5 +34,6 @@ export function getMockInventoryContext(): InventoryKibanaContext { fetch: jest.fn(), stream: jest.fn(), }, + spaces: {} as unknown as SpacesPluginStart, }; } diff --git a/x-pack/plugins/observability_solution/inventory/common/entities.ts b/x-pack/plugins/observability_solution/inventory/common/entities.ts index 01f17807c3486..40fae48cb9dc3 100644 --- a/x-pack/plugins/observability_solution/inventory/common/entities.ts +++ b/x-pack/plugins/observability_solution/inventory/common/entities.ts @@ -6,15 +6,16 @@ */ import { ENTITY_LATEST, entitiesAliasPattern } from '@kbn/entities-schema'; import { - CONTAINER_ID, - HOST_NAME, AGENT_NAME, CLOUD_PROVIDER, + CONTAINER_ID, ENTITY_DEFINITION_ID, ENTITY_DISPLAY_NAME, ENTITY_ID, + ENTITY_IDENTITY_FIELDS, ENTITY_LAST_SEEN, ENTITY_TYPE, + HOST_NAME, SERVICE_ENVIRONMENT, SERVICE_NAME, } from '@kbn/observability-shared-plugin/common'; @@ -77,25 +78,27 @@ interface BaseEntity { [ENTITY_TYPE]: EntityType; [ENTITY_DISPLAY_NAME]: string; [ENTITY_DEFINITION_ID]: string; + [ENTITY_IDENTITY_FIELDS]: string | string[]; + [key: string]: any; } /** * These types are based on service, host and container from the built in definition. */ -interface ServiceEntity extends BaseEntity { +export interface ServiceEntity extends BaseEntity { [ENTITY_TYPE]: 'service'; [SERVICE_NAME]: string; - [SERVICE_ENVIRONMENT]?: string | null; + [SERVICE_ENVIRONMENT]?: string | string[] | null; [AGENT_NAME]: string | string[] | null; } -interface HostEntity extends BaseEntity { +export interface HostEntity extends BaseEntity { [ENTITY_TYPE]: 'host'; [HOST_NAME]: string; [CLOUD_PROVIDER]: string | string[] | null; } -interface ContainerEntity extends BaseEntity { +export interface ContainerEntity extends BaseEntity { [ENTITY_TYPE]: 'container'; [CONTAINER_ID]: string; [CLOUD_PROVIDER]: string | string[] | null; diff --git a/x-pack/plugins/observability_solution/inventory/kibana.jsonc b/x-pack/plugins/observability_solution/inventory/kibana.jsonc index f60cf36183b24..28556c7bcc583 100644 --- a/x-pack/plugins/observability_solution/inventory/kibana.jsonc +++ b/x-pack/plugins/observability_solution/inventory/kibana.jsonc @@ -19,7 +19,7 @@ "share" ], "requiredBundles": ["kibanaReact"], - "optionalPlugins": [], + "optionalPlugins": ["spaces"], "extraPublicDirs": [] } } diff --git a/x-pack/plugins/observability_solution/inventory/public/application.tsx b/x-pack/plugins/observability_solution/inventory/public/application.tsx index d34be920d68ff..7b611d1d04c22 100644 --- a/x-pack/plugins/observability_solution/inventory/public/application.tsx +++ b/x-pack/plugins/observability_solution/inventory/public/application.tsx @@ -6,9 +6,8 @@ */ import React from 'react'; import ReactDOM from 'react-dom'; -import { APP_WRAPPER_CLASS, type AppMountParameters, type CoreStart } from '@kbn/core/public'; +import { type AppMountParameters, type CoreStart } from '@kbn/core/public'; import { KibanaRenderContextProvider } from '@kbn/react-kibana-context-render'; -import { css } from '@emotion/css'; import type { InventoryStartDependencies } from './types'; import { InventoryServices } from './services/types'; import { AppRoot } from './components/app_root'; @@ -25,12 +24,6 @@ export const renderApp = ({ } & { appMountParameters: AppMountParameters }) => { const { element } = appMountParameters; - const appWrapperClassName = css` - overflow: auto; - `; - const appWrapperElement = document.getElementsByClassName(APP_WRAPPER_CLASS)[1]; - appWrapperElement.classList.add(appWrapperClassName); - ReactDOM.render( { ReactDOM.unmountComponentAtNode(element); - appWrapperElement.classList.remove(APP_WRAPPER_CLASS); }; }; diff --git a/x-pack/plugins/observability_solution/inventory/public/components/entities_grid/entity_name/entity_name.test.tsx b/x-pack/plugins/observability_solution/inventory/public/components/entities_grid/entity_name/entity_name.test.tsx new file mode 100644 index 0000000000000..36aad3d8e3a97 --- /dev/null +++ b/x-pack/plugins/observability_solution/inventory/public/components/entities_grid/entity_name/entity_name.test.tsx @@ -0,0 +1,152 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license 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 KibanaReactContextValue } from '@kbn/kibana-react-plugin/public'; +import * as useKibana from '../../../hooks/use_kibana'; +import { EntityName } from '.'; +import { ContainerEntity, HostEntity, ServiceEntity } from '../../../../common/entities'; +import { render, screen } from '@testing-library/react'; +import React from 'react'; +import { ASSET_DETAILS_LOCATOR_ID } from '@kbn/observability-shared-plugin/common/locators/infra/asset_details_locator'; + +describe('EntityName', () => { + jest.spyOn(useKibana, 'useKibana').mockReturnValue({ + services: { + share: { + url: { + locators: { + get: (locatorId: string) => { + return { + getRedirectUrl: (params: { [key: string]: any }) => { + if (locatorId === ASSET_DETAILS_LOCATOR_ID) { + return `assets_url/${params.assetType}/${params.assetId}`; + } + return `services_url/${params.serviceName}?environment=${params.environment}`; + }, + }; + }, + }, + }, + }, + }, + } as unknown as KibanaReactContextValue); + + afterAll(() => { + jest.clearAllMocks(); + }); + + it('returns host link', () => { + const entity: HostEntity = { + 'entity.lastSeenTimestamp': 'foo', + 'entity.id': '1', + 'entity.type': 'host', + 'entity.displayName': 'foo', + 'entity.identityFields': 'host.name', + 'host.name': 'foo', + 'entity.definitionId': 'host', + 'cloud.provider': null, + }; + render(); + expect(screen.queryByTestId('entityNameLink')?.getAttribute('href')).toEqual( + 'assets_url/host/foo' + ); + expect(screen.queryByTestId('entityNameDisplayName')?.textContent).toEqual('foo'); + }); + + it('returns container link', () => { + const entity: ContainerEntity = { + 'entity.lastSeenTimestamp': 'foo', + 'entity.id': '1', + 'entity.type': 'container', + 'entity.displayName': 'foo', + 'entity.identityFields': 'container.id', + 'container.id': 'foo', + 'entity.definitionId': 'container', + 'cloud.provider': null, + }; + render(); + expect(screen.queryByTestId('entityNameLink')?.getAttribute('href')).toEqual( + 'assets_url/container/foo' + ); + expect(screen.queryByTestId('entityNameDisplayName')?.textContent).toEqual('foo'); + }); + + it('returns service link without environment', () => { + const entity: ServiceEntity = { + 'entity.lastSeenTimestamp': 'foo', + 'entity.id': '1', + 'entity.type': 'service', + 'entity.displayName': 'foo', + 'entity.identityFields': 'service.name', + 'service.name': 'foo', + 'entity.definitionId': 'service', + 'agent.name': 'bar', + }; + render(); + expect(screen.queryByTestId('entityNameLink')?.getAttribute('href')).toEqual( + 'services_url/foo?environment=undefined' + ); + expect(screen.queryByTestId('entityNameDisplayName')?.textContent).toEqual('foo'); + }); + + it('returns service link with environment', () => { + const entity: ServiceEntity = { + 'entity.lastSeenTimestamp': 'foo', + 'entity.id': '1', + 'entity.type': 'service', + 'entity.displayName': 'foo', + 'entity.identityFields': 'service.name', + 'service.name': 'foo', + 'entity.definitionId': 'service', + 'agent.name': 'bar', + 'service.environment': 'baz', + }; + render(); + expect(screen.queryByTestId('entityNameLink')?.getAttribute('href')).toEqual( + 'services_url/foo?environment=baz' + ); + expect(screen.queryByTestId('entityNameDisplayName')?.textContent).toEqual('foo'); + }); + + it('returns service link with first environment when it is an array', () => { + const entity: ServiceEntity = { + 'entity.lastSeenTimestamp': 'foo', + 'entity.id': '1', + 'entity.type': 'service', + 'entity.displayName': 'foo', + 'entity.identityFields': 'service.name', + 'service.name': 'foo', + 'entity.definitionId': 'service', + 'agent.name': 'bar', + 'service.environment': ['baz', 'bar', 'foo'], + }; + render(); + expect(screen.queryByTestId('entityNameLink')?.getAttribute('href')).toEqual( + 'services_url/foo?environment=baz' + ); + expect(screen.queryByTestId('entityNameDisplayName')?.textContent).toEqual('foo'); + }); + + it('returns service link identity fields is an array', () => { + const entity: ServiceEntity = { + 'entity.lastSeenTimestamp': 'foo', + 'entity.id': '1', + 'entity.type': 'service', + 'entity.displayName': 'foo', + 'entity.identityFields': ['service.name', 'service.environment'], + 'service.name': 'foo', + 'entity.definitionId': 'service', + 'agent.name': 'bar', + 'service.environment': 'baz', + }; + render(); + expect(screen.queryByTestId('entityNameLink')?.getAttribute('href')).toEqual( + 'services_url/foo?environment=baz' + ); + expect(screen.queryByTestId('entityNameDisplayName')?.textContent).toEqual('foo'); + }); +}); diff --git a/x-pack/plugins/observability_solution/inventory/public/components/entities_grid/entity_name/index.tsx b/x-pack/plugins/observability_solution/inventory/public/components/entities_grid/entity_name/index.tsx index 28fe38511fa9f..f3488dfddbc4e 100644 --- a/x-pack/plugins/observability_solution/inventory/public/components/entities_grid/entity_name/index.tsx +++ b/x-pack/plugins/observability_solution/inventory/public/components/entities_grid/entity_name/index.tsx @@ -5,19 +5,20 @@ * 2.0. */ -import { EuiLink, EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; -import React, { useCallback } from 'react'; +import { EuiFlexGroup, EuiFlexItem, EuiLink } from '@elastic/eui'; import { - AssetDetailsLocatorParams, ASSET_DETAILS_LOCATOR_ID, - ServiceOverviewParams, - ENTITY_TYPE, + AssetDetailsLocatorParams, ENTITY_DISPLAY_NAME, + ENTITY_IDENTITY_FIELDS, + ENTITY_TYPE, + SERVICE_ENVIRONMENT, + ServiceOverviewParams, } from '@kbn/observability-shared-plugin/common'; +import React, { useCallback } from 'react'; +import { Entity } from '../../../../common/entities'; import { useKibana } from '../../../hooks/use_kibana'; import { EntityIcon } from '../../entity_icon'; -import { Entity } from '../../../../common/entities'; -import { parseServiceParams } from '../../../utils/parse_service_params'; interface EntityNameProps { entity: Entity; @@ -34,34 +35,38 @@ export function EntityName({ entity }: EntityNameProps) { const getEntityRedirectUrl = useCallback(() => { const type = entity[ENTITY_TYPE]; + // For service, host and container type there is only one identity field + const identityField = Array.isArray(entity[ENTITY_IDENTITY_FIELDS]) + ? entity[ENTITY_IDENTITY_FIELDS][0] + : entity[ENTITY_IDENTITY_FIELDS]; + const identityValue = entity[identityField]; - // Any unrecognised types will always return undefined switch (type) { case 'host': case 'container': return assetDetailsLocator?.getRedirectUrl({ - assetId: entity[ENTITY_DISPLAY_NAME], + assetId: identityValue, assetType: type, }); case 'service': - // For services, the format of the display name is `service.name:service.environment`. - // We just want the first part of the name for the locator. - // TODO: Replace this with a better approach for handling service names. See https://github.com/elastic/kibana/issues/194131 - return serviceOverviewLocator?.getRedirectUrl( - parseServiceParams(entity[ENTITY_DISPLAY_NAME]) - ); + return serviceOverviewLocator?.getRedirectUrl({ + serviceName: identityValue, + environment: [entity[SERVICE_ENVIRONMENT] || undefined].flat()[0], + }); } }, [entity, assetDetailsLocator, serviceOverviewLocator]); return ( - + - {entity[ENTITY_DISPLAY_NAME]} + + {entity[ENTITY_DISPLAY_NAME]} + diff --git a/x-pack/plugins/observability_solution/inventory/public/plugin.ts b/x-pack/plugins/observability_solution/inventory/public/plugin.ts index c02a57b45f691..30e3a1eed3681 100644 --- a/x-pack/plugins/observability_solution/inventory/public/plugin.ts +++ b/x-pack/plugins/observability_solution/inventory/public/plugin.ts @@ -17,7 +17,7 @@ import { import { INVENTORY_APP_ID } from '@kbn/deeplinks-observability/constants'; import { i18n } from '@kbn/i18n'; import type { Logger } from '@kbn/logging'; -import { from, map } from 'rxjs'; +import { from, map, mergeMap, of } from 'rxjs'; import { createCallInventoryAPI } from './api'; import { TelemetryService } from './services/telemetry/telemetry_service'; import { InventoryServices } from './services/types'; @@ -54,34 +54,53 @@ export class InventoryPlugin 'observability:entityCentricExperience', true ); + const getStartServices = coreSetup.getStartServices(); - if (isEntityCentricExperienceSettingEnabled) { - pluginsSetup.observabilityShared.navigation.registerSections( - from(coreSetup.getStartServices()).pipe( - map(([coreStart, pluginsStart]) => { - return [ - { - label: '', - sortKey: 300, - entries: [ - { - label: i18n.translate('xpack.inventory.inventoryLinkTitle', { - defaultMessage: 'Inventory', - }), - app: INVENTORY_APP_ID, - path: '/', - matchPath(currentPath: string) { - return ['/', ''].some((testPath) => currentPath.startsWith(testPath)); - }, - isTechnicalPreview: true, + const hideInventory$ = from(getStartServices).pipe( + mergeMap(([coreStart, pluginsStart]) => { + if (pluginsStart.spaces) { + return from(pluginsStart.spaces.getActiveSpace()).pipe( + map( + (space) => + space.disabledFeatures.includes(INVENTORY_APP_ID) || + !coreStart.application.capabilities.inventory.show + ) + ); + } + + return of(!coreStart.application.capabilities.inventory.show); + }) + ); + + const sections$ = hideInventory$.pipe( + map((hideInventory) => { + if (isEntityCentricExperienceSettingEnabled && !hideInventory) { + return [ + { + label: '', + sortKey: 300, + entries: [ + { + label: i18n.translate('xpack.inventory.inventoryLinkTitle', { + defaultMessage: 'Inventory', + }), + app: INVENTORY_APP_ID, + path: '/', + matchPath(currentPath: string) { + return ['/', ''].some((testPath) => currentPath.startsWith(testPath)); }, - ], - }, - ]; - }) - ) - ); - } + isTechnicalPreview: true, + }, + ], + }, + ]; + } + return []; + }) + ); + + pluginsSetup.observabilityShared.navigation.registerSections(sections$); + this.telemetry.setup({ analytics: coreSetup.analytics }); const telemetry = this.telemetry.start(); @@ -102,7 +121,7 @@ export class InventoryPlugin // Load application bundle and Get start services const [{ renderApp }, [coreStart, pluginsStart]] = await Promise.all([ import('./application'), - coreSetup.getStartServices(), + getStartServices, ]); const services: InventoryServices = { diff --git a/x-pack/plugins/observability_solution/inventory/public/types.ts b/x-pack/plugins/observability_solution/inventory/public/types.ts index 2393b1b55e2b6..48fe7e7eed1c7 100644 --- a/x-pack/plugins/observability_solution/inventory/public/types.ts +++ b/x-pack/plugins/observability_solution/inventory/public/types.ts @@ -17,6 +17,7 @@ import type { SharePluginSetup, SharePluginStart } from '@kbn/share-plugin/publi import type { UnifiedSearchPublicPluginStart } from '@kbn/unified-search-plugin/public'; import type { DataViewsPublicPluginStart } from '@kbn/data-views-plugin/public'; import type { DataPublicPluginSetup, DataPublicPluginStart } from '@kbn/data-plugin/public'; +import type { SpacesPluginStart } from '@kbn/spaces-plugin/public'; /* eslint-disable @typescript-eslint/no-empty-interface*/ @@ -38,6 +39,7 @@ export interface InventoryStartDependencies { data: DataPublicPluginStart; entityManager: EntityManagerPublicPluginStart; share: SharePluginStart; + spaces?: SpacesPluginStart; } export interface InventoryPublicSetup {} diff --git a/x-pack/plugins/observability_solution/inventory/public/utils/parse_service_params.test.ts b/x-pack/plugins/observability_solution/inventory/public/utils/parse_service_params.test.ts deleted file mode 100644 index 217b28480feb1..0000000000000 --- a/x-pack/plugins/observability_solution/inventory/public/utils/parse_service_params.test.ts +++ /dev/null @@ -1,39 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { parseServiceParams } from './parse_service_params'; - -describe('parseServiceParams', () => { - it('should return only serviceName with a simple name string', () => { - const params = parseServiceParams('service.name'); - - expect(params).toEqual({ serviceName: 'service.name' }); - }); - - it('should return both serviceName and environment with a full name string', () => { - const params = parseServiceParams('service.name:service.environment'); - - expect(params).toEqual({ serviceName: 'service.name', environment: 'service.environment' }); - }); - - it('should ignore multiple colons in the environment portion of the displayName', () => { - const params = parseServiceParams('service.name:synthtrace: service.environment'); - - expect(params).toEqual({ - serviceName: 'service.name', - environment: 'synthtrace: service.environment', - }); - }); - - it('should ignore empty environment names and return only the service.name', () => { - const params = parseServiceParams('service.name:'); - - expect(params).toEqual({ - serviceName: 'service.name', - }); - }); -}); diff --git a/x-pack/plugins/observability_solution/inventory/public/utils/parse_service_params.ts b/x-pack/plugins/observability_solution/inventory/public/utils/parse_service_params.ts deleted file mode 100644 index 637957c578272..0000000000000 --- a/x-pack/plugins/observability_solution/inventory/public/utils/parse_service_params.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; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import type { ServiceOverviewParams } from '@kbn/observability-shared-plugin/common'; - -/** - * Parses a displayName string with the format `service.name:service.environment`, - * returning a valid `ServiceOverviewParams` object. - * @param displayName A string from a `entity.displayName` field. - * @returns - */ -export const parseServiceParams = (displayName: string): ServiceOverviewParams => { - const separatorIndex = displayName.indexOf(':'); - - const hasEnvironmentName = separatorIndex !== -1; - - const serviceName = hasEnvironmentName ? displayName.slice(0, separatorIndex) : displayName; - // Exclude the separator from the sliced string for the environment name. - // If the string is empty however, then we default to undefined. - const environment = (hasEnvironmentName && displayName.slice(separatorIndex + 1)) || undefined; - - return { - serviceName, - environment, - }; -}; diff --git a/x-pack/plugins/observability_solution/inventory/tsconfig.json b/x-pack/plugins/observability_solution/inventory/tsconfig.json index 54fcfe7e3a11f..20b5e2e37232a 100644 --- a/x-pack/plugins/observability_solution/inventory/tsconfig.json +++ b/x-pack/plugins/observability_solution/inventory/tsconfig.json @@ -45,6 +45,7 @@ "@kbn/config-schema", "@kbn/elastic-agent-utils", "@kbn/custom-icons", - "@kbn/ui-theme" + "@kbn/ui-theme", + "@kbn/spaces-plugin" ] } diff --git a/x-pack/plugins/observability_solution/investigate_app/server/services/get_entities.ts b/x-pack/plugins/observability_solution/investigate_app/server/services/get_entities.ts index 8f3a0abb62b67..00151f2029d21 100644 --- a/x-pack/plugins/observability_solution/investigate_app/server/services/get_entities.ts +++ b/x-pack/plugins/observability_solution/investigate_app/server/services/get_entities.ts @@ -61,7 +61,6 @@ export async function getEntitiesWithSource({ identityFields: entity?.entity.identityFields, id: entity?.entity.id, definitionId: entity?.entity.definitionId, - firstSeenTimestamp: entity?.entity.firstSeenTimestamp, lastSeenTimestamp: entity?.entity.lastSeenTimestamp, displayName: entity?.entity.displayName, metrics: entity?.entity.metrics, diff --git a/x-pack/plugins/observability_solution/logs_shared/kibana.jsonc b/x-pack/plugins/observability_solution/logs_shared/kibana.jsonc index ea93fd326dac7..10c8fe32cfe9c 100644 --- a/x-pack/plugins/observability_solution/logs_shared/kibana.jsonc +++ b/x-pack/plugins/observability_solution/logs_shared/kibana.jsonc @@ -9,13 +9,14 @@ "browser": true, "configPath": ["xpack", "logs_shared"], "requiredPlugins": [ + "charts", "data", "dataViews", "discoverShared", - "usageCollection", + "logsDataAccess", "observabilityShared", "share", - "logsDataAccess" + "usageCollection", ], "optionalPlugins": [ "observabilityAIAssistant", diff --git a/x-pack/plugins/observability_solution/logs_shared/public/components/logs_overview/index.tsx b/x-pack/plugins/observability_solution/logs_shared/public/components/logs_overview/index.tsx new file mode 100644 index 0000000000000..627cdc8447eea --- /dev/null +++ b/x-pack/plugins/observability_solution/logs_shared/public/components/logs_overview/index.tsx @@ -0,0 +1,8 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +export * from './logs_overview'; diff --git a/x-pack/plugins/observability_solution/logs_shared/public/components/logs_overview/logs_overview.mock.tsx b/x-pack/plugins/observability_solution/logs_shared/public/components/logs_overview/logs_overview.mock.tsx new file mode 100644 index 0000000000000..435766bff793d --- /dev/null +++ b/x-pack/plugins/observability_solution/logs_shared/public/components/logs_overview/logs_overview.mock.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 from 'react'; +import type { + LogsOverviewProps, + SelfContainedLogsOverviewComponent, + SelfContainedLogsOverviewHelpers, +} from './logs_overview'; + +export const createLogsOverviewMock = () => { + const LogsOverviewMock = jest.fn(LogsOverviewMockImpl) as unknown as ILogsOverviewMock; + + LogsOverviewMock.useIsEnabled = jest.fn(() => true); + + LogsOverviewMock.ErrorContent = jest.fn(() =>
    ); + + LogsOverviewMock.LoadingContent = jest.fn(() =>
    ); + + return LogsOverviewMock; +}; + +const LogsOverviewMockImpl = (_props: LogsOverviewProps) => { + return
    ; +}; + +type ILogsOverviewMock = jest.Mocked & + jest.Mocked; diff --git a/x-pack/plugins/observability_solution/logs_shared/public/components/logs_overview/logs_overview.tsx b/x-pack/plugins/observability_solution/logs_shared/public/components/logs_overview/logs_overview.tsx new file mode 100644 index 0000000000000..7b60aee5be57c --- /dev/null +++ b/x-pack/plugins/observability_solution/logs_shared/public/components/logs_overview/logs_overview.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 { OBSERVABILITY_LOGS_SHARED_NEW_LOGS_OVERVIEW_ID } from '@kbn/management-settings-ids'; +import type { + LogsOverviewProps as FullLogsOverviewProps, + LogsOverviewDependencies, + LogsOverviewErrorContentProps, +} from '@kbn/observability-logs-overview'; +import { dynamic } from '@kbn/shared-ux-utility'; +import React from 'react'; +import useObservable from 'react-use/lib/useObservable'; + +const LazyLogsOverview = dynamic(() => + import('@kbn/observability-logs-overview').then((mod) => ({ default: mod.LogsOverview })) +); + +const LazyLogsOverviewErrorContent = dynamic(() => + import('@kbn/observability-logs-overview').then((mod) => ({ + default: mod.LogsOverviewErrorContent, + })) +); + +const LazyLogsOverviewLoadingContent = dynamic(() => + import('@kbn/observability-logs-overview').then((mod) => ({ + default: mod.LogsOverviewLoadingContent, + })) +); + +export type LogsOverviewProps = Omit; + +export interface SelfContainedLogsOverviewHelpers { + useIsEnabled: () => boolean; + ErrorContent: React.ComponentType; + LoadingContent: React.ComponentType; +} + +export type SelfContainedLogsOverviewComponent = React.ComponentType; + +export type SelfContainedLogsOverview = SelfContainedLogsOverviewComponent & + SelfContainedLogsOverviewHelpers; + +export const createLogsOverview = ( + dependencies: LogsOverviewDependencies +): SelfContainedLogsOverview => { + const SelfContainedLogsOverview = (props: LogsOverviewProps) => { + return ; + }; + + const isEnabled$ = dependencies.uiSettings.client.get$( + OBSERVABILITY_LOGS_SHARED_NEW_LOGS_OVERVIEW_ID, + defaultIsEnabled + ); + + SelfContainedLogsOverview.useIsEnabled = (): boolean => { + return useObservable(isEnabled$, defaultIsEnabled); + }; + + SelfContainedLogsOverview.ErrorContent = LazyLogsOverviewErrorContent; + + SelfContainedLogsOverview.LoadingContent = LazyLogsOverviewLoadingContent; + + return SelfContainedLogsOverview; +}; + +const defaultIsEnabled = false; diff --git a/x-pack/plugins/observability_solution/logs_shared/public/index.ts b/x-pack/plugins/observability_solution/logs_shared/public/index.ts index a602b25786116..3d601c9936f2d 100644 --- a/x-pack/plugins/observability_solution/logs_shared/public/index.ts +++ b/x-pack/plugins/observability_solution/logs_shared/public/index.ts @@ -50,6 +50,7 @@ export type { UpdatedDateRange, VisibleInterval, } from './components/logging/log_text_stream/scrollable_log_text_stream_view'; +export type { LogsOverviewProps } from './components/logs_overview'; export const WithSummary = dynamic(() => import('./containers/logs/log_summary/with_summary')); export const LogEntryFlyout = dynamic( diff --git a/x-pack/plugins/observability_solution/logs_shared/public/mocks.tsx b/x-pack/plugins/observability_solution/logs_shared/public/mocks.tsx index a9b0ebd6a6aa3..ffb867abbcc17 100644 --- a/x-pack/plugins/observability_solution/logs_shared/public/mocks.tsx +++ b/x-pack/plugins/observability_solution/logs_shared/public/mocks.tsx @@ -6,12 +6,14 @@ */ import { createLogAIAssistantMock } from './components/log_ai_assistant/log_ai_assistant.mock'; +import { createLogsOverviewMock } from './components/logs_overview/logs_overview.mock'; import { createLogViewsServiceStartMock } from './services/log_views/log_views_service.mock'; import { LogsSharedClientStartExports } from './types'; export const createLogsSharedPluginStartMock = (): jest.Mocked => ({ logViews: createLogViewsServiceStartMock(), LogAIAssistant: createLogAIAssistantMock(), + LogsOverview: createLogsOverviewMock(), }); export const _ensureTypeCompatibility = (): LogsSharedClientStartExports => diff --git a/x-pack/plugins/observability_solution/logs_shared/public/plugin.ts b/x-pack/plugins/observability_solution/logs_shared/public/plugin.ts index d6f4ac81fe266..fc17e9b17cc82 100644 --- a/x-pack/plugins/observability_solution/logs_shared/public/plugin.ts +++ b/x-pack/plugins/observability_solution/logs_shared/public/plugin.ts @@ -12,6 +12,7 @@ import { TraceLogsLocatorDefinition, } from '../common/locators'; import { createLogAIAssistant, createLogsAIAssistantRenderer } from './components/log_ai_assistant'; +import { createLogsOverview } from './components/logs_overview'; import { LogViewsService } from './services/log_views'; import { LogsSharedClientCoreSetup, @@ -51,8 +52,16 @@ export class LogsSharedPlugin implements LogsSharedClientPluginClass { } public start(core: CoreStart, plugins: LogsSharedClientStartDeps) { - const { http } = core; - const { data, dataViews, discoverShared, observabilityAIAssistant, logsDataAccess } = plugins; + const { http, settings } = core; + const { + charts, + data, + dataViews, + discoverShared, + logsDataAccess, + observabilityAIAssistant, + share, + } = plugins; const logViews = this.logViews.start({ http, @@ -61,9 +70,18 @@ export class LogsSharedPlugin implements LogsSharedClientPluginClass { search: data.search, }); + const LogsOverview = createLogsOverview({ + charts, + logsDataAccess, + search: data.search.search, + uiSettings: settings, + share, + }); + if (!observabilityAIAssistant) { return { logViews, + LogsOverview, }; } @@ -77,6 +95,7 @@ export class LogsSharedPlugin implements LogsSharedClientPluginClass { return { logViews, LogAIAssistant, + LogsOverview, }; } diff --git a/x-pack/plugins/observability_solution/logs_shared/public/types.ts b/x-pack/plugins/observability_solution/logs_shared/public/types.ts index 58b180ee8b6ef..4237c28c621b8 100644 --- a/x-pack/plugins/observability_solution/logs_shared/public/types.ts +++ b/x-pack/plugins/observability_solution/logs_shared/public/types.ts @@ -5,19 +5,19 @@ * 2.0. */ +import type { ChartsPluginStart } from '@kbn/charts-plugin/public'; import type { CoreSetup, CoreStart, Plugin as PluginClass } from '@kbn/core/public'; import type { DataPublicPluginStart } from '@kbn/data-plugin/public'; import type { DataViewsPublicPluginStart } from '@kbn/data-views-plugin/public'; import type { DiscoverSharedPublicStart } from '@kbn/discover-shared-plugin/public'; -import { LogsDataAccessPluginStart } from '@kbn/logs-data-access-plugin/public'; +import type { LogsDataAccessPluginStart } from '@kbn/logs-data-access-plugin/public'; import type { ObservabilityAIAssistantPublicStart } from '@kbn/observability-ai-assistant-plugin/public'; import type { SharePluginSetup, SharePluginStart } from '@kbn/share-plugin/public'; import type { UiActionsStart } from '@kbn/ui-actions-plugin/public'; - -import { LogsSharedLocators } from '../common/locators'; +import type { LogsSharedLocators } from '../common/locators'; import type { LogAIAssistantProps } from './components/log_ai_assistant/log_ai_assistant'; -// import type { OsqueryPluginStart } from '../../osquery/public'; -import { LogViewsServiceSetup, LogViewsServiceStart } from './services/log_views'; +import type { SelfContainedLogsOverview } from './components/logs_overview'; +import type { LogViewsServiceSetup, LogViewsServiceStart } from './services/log_views'; // Our own setup and start contract values export interface LogsSharedClientSetupExports { @@ -28,6 +28,7 @@ export interface LogsSharedClientSetupExports { export interface LogsSharedClientStartExports { logViews: LogViewsServiceStart; LogAIAssistant?: (props: Omit) => JSX.Element; + LogsOverview: SelfContainedLogsOverview; } export interface LogsSharedClientSetupDeps { @@ -35,6 +36,7 @@ export interface LogsSharedClientSetupDeps { } export interface LogsSharedClientStartDeps { + charts: ChartsPluginStart; data: DataPublicPluginStart; dataViews: DataViewsPublicPluginStart; discoverShared: DiscoverSharedPublicStart; diff --git a/x-pack/plugins/observability_solution/logs_shared/server/feature_flags.ts b/x-pack/plugins/observability_solution/logs_shared/server/feature_flags.ts new file mode 100644 index 0000000000000..0298416bd3f26 --- /dev/null +++ b/x-pack/plugins/observability_solution/logs_shared/server/feature_flags.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 { schema } from '@kbn/config-schema'; +import { UiSettingsParams } from '@kbn/core-ui-settings-common'; +import { i18n } from '@kbn/i18n'; +import { OBSERVABILITY_LOGS_SHARED_NEW_LOGS_OVERVIEW_ID } from '@kbn/management-settings-ids'; + +const technicalPreviewLabel = i18n.translate('xpack.logsShared.technicalPreviewSettingLabel', { + defaultMessage: 'Technical Preview', +}); + +export const featureFlagUiSettings: Record = { + [OBSERVABILITY_LOGS_SHARED_NEW_LOGS_OVERVIEW_ID]: { + category: ['observability'], + name: i18n.translate('xpack.logsShared.newLogsOverviewSettingName', { + defaultMessage: 'New logs overview', + }), + value: false, + description: i18n.translate('xpack.logsShared.newLogsOverviewSettingDescription', { + defaultMessage: '{technicalPreviewLabel} Enable the new logs overview experience.', + + values: { technicalPreviewLabel: `[${technicalPreviewLabel}]` }, + }), + type: 'boolean', + schema: schema.boolean(), + requiresPageReload: true, + }, +}; diff --git a/x-pack/plugins/observability_solution/logs_shared/server/plugin.ts b/x-pack/plugins/observability_solution/logs_shared/server/plugin.ts index 7c97e175ed64f..d1f6399104fc2 100644 --- a/x-pack/plugins/observability_solution/logs_shared/server/plugin.ts +++ b/x-pack/plugins/observability_solution/logs_shared/server/plugin.ts @@ -5,8 +5,19 @@ * 2.0. */ -import { PluginInitializerContext, CoreStart, Plugin, Logger } from '@kbn/core/server'; - +import { CoreStart, Logger, Plugin, PluginInitializerContext } from '@kbn/core/server'; +import { defaultLogViewId } from '../common/log_views'; +import { LogsSharedConfig } from '../common/plugin_config'; +import { registerDeprecations } from './deprecations'; +import { featureFlagUiSettings } from './feature_flags'; +import { KibanaFramework } from './lib/adapters/framework/kibana_framework_adapter'; +import { LogsSharedKibanaLogEntriesAdapter } from './lib/adapters/log_entries/kibana_log_entries_adapter'; +import { LogsSharedLogEntriesDomain } from './lib/domains/log_entries_domain'; +import { LogsSharedBackendLibs, LogsSharedDomainLibs } from './lib/logs_shared_types'; +import { initLogsSharedServer } from './logs_shared_server'; +import { logViewSavedObjectType } from './saved_objects'; +import { LogEntriesService } from './services/log_entries'; +import { LogViewsService } from './services/log_views'; import { LogsSharedPluginCoreSetup, LogsSharedPluginSetup, @@ -15,17 +26,6 @@ import { LogsSharedServerPluginStartDeps, UsageCollector, } from './types'; -import { logViewSavedObjectType } from './saved_objects'; -import { initLogsSharedServer } from './logs_shared_server'; -import { LogViewsService } from './services/log_views'; -import { KibanaFramework } from './lib/adapters/framework/kibana_framework_adapter'; -import { LogsSharedBackendLibs, LogsSharedDomainLibs } from './lib/logs_shared_types'; -import { LogsSharedLogEntriesDomain } from './lib/domains/log_entries_domain'; -import { LogsSharedKibanaLogEntriesAdapter } from './lib/adapters/log_entries/kibana_log_entries_adapter'; -import { LogEntriesService } from './services/log_entries'; -import { LogsSharedConfig } from '../common/plugin_config'; -import { registerDeprecations } from './deprecations'; -import { defaultLogViewId } from '../common/log_views'; export class LogsSharedPlugin implements @@ -88,6 +88,8 @@ export class LogsSharedPlugin registerDeprecations({ core }); + core.uiSettings.register(featureFlagUiSettings); + return { ...domainLibs, logViews, diff --git a/x-pack/plugins/observability_solution/logs_shared/tsconfig.json b/x-pack/plugins/observability_solution/logs_shared/tsconfig.json index 38cbba7c252c0..788f55c9b6fc5 100644 --- a/x-pack/plugins/observability_solution/logs_shared/tsconfig.json +++ b/x-pack/plugins/observability_solution/logs_shared/tsconfig.json @@ -44,5 +44,9 @@ "@kbn/logs-data-access-plugin", "@kbn/core-deprecations-common", "@kbn/core-deprecations-server", + "@kbn/management-settings-ids", + "@kbn/observability-logs-overview", + "@kbn/charts-plugin", + "@kbn/core-ui-settings-common", ] } diff --git a/x-pack/plugins/observability_solution/observability/public/navigation_tree.ts b/x-pack/plugins/observability_solution/observability/public/navigation_tree.ts index c661976fd0765..0c6ceede07561 100644 --- a/x-pack/plugins/observability_solution/observability/public/navigation_tree.ts +++ b/x-pack/plugins/observability_solution/observability/public/navigation_tree.ts @@ -366,6 +366,12 @@ export function createNavTree(pluginsStart: ObservabilityPublicPluginsStart) { defaultMessage: 'Logs categories', }), }, + { + link: 'logs:settings', + title: i18n.translate('xpack.observability.obltNav.otherTools.logsSettings', { + defaultMessage: 'Logs settings', + }), + }, { link: 'maps' }, { link: 'canvas' }, { link: 'graph' }, diff --git a/x-pack/plugins/observability_solution/observability/tsconfig.json b/x-pack/plugins/observability_solution/observability/tsconfig.json index d7a33cb6492cb..cc8cef2a9716a 100644 --- a/x-pack/plugins/observability_solution/observability/tsconfig.json +++ b/x-pack/plugins/observability_solution/observability/tsconfig.json @@ -113,7 +113,7 @@ "@kbn/io-ts-utils", "@kbn/core-ui-settings-server-mocks", "@kbn/es-types", - "@kbn/logging-mocks" + "@kbn/logging-mocks", ], "exclude": ["target/**/*"] } diff --git a/x-pack/plugins/observability_solution/observability_ai_assistant/public/assets/elastic_ai_assistant.png b/x-pack/plugins/observability_solution/observability_ai_assistant/public/assets/elastic_ai_assistant.png new file mode 100644 index 0000000000000..af10645579683 Binary files /dev/null and b/x-pack/plugins/observability_solution/observability_ai_assistant/public/assets/elastic_ai_assistant.png differ diff --git a/x-pack/plugins/observability_solution/observability_ai_assistant/public/index.ts b/x-pack/plugins/observability_solution/observability_ai_assistant/public/index.ts index ab2dea089dcf1..76e643c6ae0d5 100644 --- a/x-pack/plugins/observability_solution/observability_ai_assistant/public/index.ts +++ b/x-pack/plugins/observability_solution/observability_ai_assistant/public/index.ts @@ -19,6 +19,7 @@ import type { RenderFunction, DiscoveredDataset, } from './types'; +import elasticAiAssistantImg from './assets/elastic_ai_assistant.png'; export type { ObservabilityAIAssistantPublicSetup, @@ -101,6 +102,8 @@ export { aiAssistantPreferredAIAssistantType, } from '../common/ui_settings/settings_keys'; +export const elasticAiAssistantImage = elasticAiAssistantImg; + export const plugin: PluginInitializer< ObservabilityAIAssistantPublicSetup, ObservabilityAIAssistantPublicStart, diff --git a/x-pack/plugins/observability_solution/observability_ai_assistant_app/public/application.tsx b/x-pack/plugins/observability_solution/observability_ai_assistant_app/public/application.tsx index c554fc81d5de7..ce043ef395ee4 100644 --- a/x-pack/plugins/observability_solution/observability_ai_assistant_app/public/application.tsx +++ b/x-pack/plugins/observability_solution/observability_ai_assistant_app/public/application.tsx @@ -9,10 +9,10 @@ import { RouteRenderer, RouterProvider } from '@kbn/typed-react-router-config'; import type { History } from 'history'; import React from 'react'; import type { Observable } from 'rxjs'; -import { observabilityAIAssistantRouter } from './routes/config'; -import type { ObservabilityAIAssistantAppService } from './service/create_app_service'; +import type { AIAssistantAppService } from '@kbn/ai-assistant'; import type { ObservabilityAIAssistantAppPluginStartDependencies } from './types'; import { SharedProviders } from './utils/shared_providers'; +import { observabilityAIAssistantRouter } from './routes/config'; // This is the Conversation application. @@ -26,7 +26,7 @@ export function Application({ coreStart: CoreStart; history: History; pluginsStart: ObservabilityAIAssistantAppPluginStartDependencies; - service: ObservabilityAIAssistantAppService; + service: AIAssistantAppService; theme$: Observable; }) { return ( @@ -36,7 +36,7 @@ export function Application({ service={service} theme$={theme$} > - + diff --git a/x-pack/plugins/observability_solution/observability_ai_assistant_app/public/components/nav_control/index.tsx b/x-pack/plugins/observability_solution/observability_ai_assistant_app/public/components/nav_control/index.tsx index 66a66ecc07dc0..2c2af65accb59 100644 --- a/x-pack/plugins/observability_solution/observability_ai_assistant_app/public/components/nav_control/index.tsx +++ b/x-pack/plugins/observability_solution/observability_ai_assistant_app/public/components/nav_control/index.tsx @@ -12,17 +12,15 @@ import { v4 } from 'uuid'; import useObservable from 'react-use/lib/useObservable'; import { i18n } from '@kbn/i18n'; import { CoreStart } from '@kbn/core-lifecycle-browser'; -import { useObservabilityAIAssistantAppService } from '../../hooks/use_observability_ai_assistant_app_service'; -import { ChatFlyout } from '../chat/chat_flyout'; +import { AIAssistantAppService, useAIAssistantAppService, ChatFlyout } from '@kbn/ai-assistant'; import { useKibana } from '../../hooks/use_kibana'; import { useTheme } from '../../hooks/use_theme'; import { useNavControlScreenContext } from '../../hooks/use_nav_control_screen_context'; import { SharedProviders } from '../../utils/shared_providers'; -import { ObservabilityAIAssistantAppService } from '../../service/create_app_service'; import { ObservabilityAIAssistantAppPluginStartDependencies } from '../../types'; interface NavControlWithProviderDeps { - appService: ObservabilityAIAssistantAppService; + appService: AIAssistantAppService; coreStart: CoreStart; pluginsStart: ObservabilityAIAssistantAppPluginStartDependencies; } @@ -45,10 +43,12 @@ export const NavControlWithProvider = ({ }; export function NavControl() { - const service = useObservabilityAIAssistantAppService(); + const service = useAIAssistantAppService(); const { services: { + application, + http, notifications, plugins: { start: { @@ -162,6 +162,13 @@ export function NavControl() { onClose={() => { setIsOpen(false); }} + navigateToConversation={(conversationId: string) => { + application.navigateToUrl( + http.basePath.prepend( + `/app/observabilityAIAssistant/conversations/${conversationId || ''}` + ) + ); + }} /> ) : undefined} diff --git a/x-pack/plugins/observability_solution/observability_ai_assistant_app/public/components/nav_control/lazy_nav_control.tsx b/x-pack/plugins/observability_solution/observability_ai_assistant_app/public/components/nav_control/lazy_nav_control.tsx index bed86909af417..adef91ceea53e 100644 --- a/x-pack/plugins/observability_solution/observability_ai_assistant_app/public/components/nav_control/lazy_nav_control.tsx +++ b/x-pack/plugins/observability_solution/observability_ai_assistant_app/public/components/nav_control/lazy_nav_control.tsx @@ -8,8 +8,8 @@ import { dynamic } from '@kbn/shared-ux-utility'; import React from 'react'; import { CoreStart } from '@kbn/core-lifecycle-browser'; +import { AIAssistantAppService } from '@kbn/ai-assistant'; import { useIsNavControlVisible } from '../../hooks/is_nav_control_visible'; -import { ObservabilityAIAssistantAppService } from '../../service/create_app_service'; import { ObservabilityAIAssistantAppPluginStartDependencies } from '../../types'; const LazyNavControlWithProvider = dynamic(() => @@ -17,7 +17,7 @@ const LazyNavControlWithProvider = dynamic(() => ); interface NavControlInitiatorProps { - appService: ObservabilityAIAssistantAppService; + appService: AIAssistantAppService; coreStart: CoreStart; pluginsStart: ObservabilityAIAssistantAppPluginStartDependencies; } diff --git a/x-pack/plugins/observability_solution/observability_ai_assistant_app/public/context/observability_ai_assistant_app_service_provider.tsx b/x-pack/plugins/observability_solution/observability_ai_assistant_app/public/context/observability_ai_assistant_app_service_provider.tsx deleted file mode 100644 index 9de7f023b4d10..0000000000000 --- a/x-pack/plugins/observability_solution/observability_ai_assistant_app/public/context/observability_ai_assistant_app_service_provider.tsx +++ /dev/null @@ -1,16 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { createContext } from 'react'; -import type { ObservabilityAIAssistantAppService } from '../service/create_app_service'; - -export const ObservabilityAIAssistantAppServiceContext = createContext< - ObservabilityAIAssistantAppService | undefined ->(undefined); - -export const ObservabilityAIAssistantAppServiceProvider = - ObservabilityAIAssistantAppServiceContext.Provider; diff --git a/x-pack/plugins/observability_solution/observability_ai_assistant_app/public/hooks/__storybook_mocks__/use_kibana.ts b/x-pack/plugins/observability_solution/observability_ai_assistant_app/public/hooks/__storybook_mocks__/use_kibana.ts index f836c3dac6159..deaabffeeb50d 100644 --- a/x-pack/plugins/observability_solution/observability_ai_assistant_app/public/hooks/__storybook_mocks__/use_kibana.ts +++ b/x-pack/plugins/observability_solution/observability_ai_assistant_app/public/hooks/__storybook_mocks__/use_kibana.ts @@ -7,10 +7,13 @@ import React from 'react'; import { Subject } from 'rxjs'; -import { useChat } from './use_chat'; const ObservabilityAIAssistantMultipaneFlyoutContext = React.createContext(undefined); +function useChat() { + return { next: () => {}, messages: [], setMessages: () => {}, state: undefined, stop: () => {} }; +} + export function useKibana() { return { services: { diff --git a/x-pack/plugins/observability_solution/observability_ai_assistant_app/public/hooks/use_nav_control_screen_context.ts b/x-pack/plugins/observability_solution/observability_ai_assistant_app/public/hooks/use_nav_control_screen_context.ts index 10195bf38651e..d068f592c4310 100644 --- a/x-pack/plugins/observability_solution/observability_ai_assistant_app/public/hooks/use_nav_control_screen_context.ts +++ b/x-pack/plugins/observability_solution/observability_ai_assistant_app/public/hooks/use_nav_control_screen_context.ts @@ -8,11 +8,11 @@ import { useEffect, useState } from 'react'; import datemath from '@elastic/datemath'; import moment from 'moment'; +import { useAIAssistantAppService } from '@kbn/ai-assistant'; import { useKibana } from './use_kibana'; -import { useObservabilityAIAssistantAppService } from './use_observability_ai_assistant_app_service'; export function useNavControlScreenContext() { - const service = useObservabilityAIAssistantAppService(); + const service = useAIAssistantAppService(); const { services: { diff --git a/x-pack/plugins/observability_solution/observability_ai_assistant_app/public/hooks/use_observability_ai_assistant_app_service.ts b/x-pack/plugins/observability_solution/observability_ai_assistant_app/public/hooks/use_observability_ai_assistant_app_service.ts deleted file mode 100644 index 9c86f29565f48..0000000000000 --- a/x-pack/plugins/observability_solution/observability_ai_assistant_app/public/hooks/use_observability_ai_assistant_app_service.ts +++ /dev/null @@ -1,20 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ -import { useContext } from 'react'; -import { ObservabilityAIAssistantAppServiceContext } from '../context/observability_ai_assistant_app_service_provider'; - -export function useObservabilityAIAssistantAppService() { - const services = useContext(ObservabilityAIAssistantAppServiceContext); - - if (!services) { - throw new Error( - 'ObservabilityAIAssistantContext not set. Did you wrap your component in ``?' - ); - } - - return services; -} diff --git a/x-pack/plugins/observability_solution/observability_ai_assistant_app/public/i18n.ts b/x-pack/plugins/observability_solution/observability_ai_assistant_app/public/i18n.ts deleted file mode 100644 index dcc28d7ff531a..0000000000000 --- a/x-pack/plugins/observability_solution/observability_ai_assistant_app/public/i18n.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 { i18n } from '@kbn/i18n'; - -export const ASSISTANT_SETUP_TITLE = i18n.translate( - 'xpack.observabilityAiAssistant.assistantSetup.title', - { - defaultMessage: 'Welcome to Elastic AI Assistant', - } -); - -export const EMPTY_CONVERSATION_TITLE = i18n.translate( - 'xpack.observabilityAiAssistant.emptyConversationTitle', - { defaultMessage: 'New conversation' } -); - -export const UPGRADE_LICENSE_TITLE = i18n.translate( - 'xpack.observabilityAiAssistant.incorrectLicense.title', - { - defaultMessage: 'Upgrade your license', - } -); diff --git a/x-pack/plugins/observability_solution/observability_ai_assistant_app/public/plugin.tsx b/x-pack/plugins/observability_solution/observability_ai_assistant_app/public/plugin.tsx index 9817cc65362d6..1904eebffb2a8 100644 --- a/x-pack/plugins/observability_solution/observability_ai_assistant_app/public/plugin.tsx +++ b/x-pack/plugins/observability_solution/observability_ai_assistant_app/public/plugin.tsx @@ -17,13 +17,13 @@ import { import type { Logger } from '@kbn/logging'; import { i18n } from '@kbn/i18n'; import { AI_ASSISTANT_APP_ID } from '@kbn/deeplinks-observability'; +import { createAppService, AIAssistantAppService } from '@kbn/ai-assistant'; import type { ObservabilityAIAssistantAppPluginSetupDependencies, ObservabilityAIAssistantAppPluginStartDependencies, ObservabilityAIAssistantAppPublicSetup, ObservabilityAIAssistantAppPublicStart, } from './types'; -import { createAppService, ObservabilityAIAssistantAppService } from './service/create_app_service'; import { getObsAIAssistantConnectorType } from './rule_connector'; import { NavControlInitiator } from './components/nav_control/lazy_nav_control'; @@ -40,7 +40,7 @@ export class ObservabilityAIAssistantAppPlugin > { logger: Logger; - appService: ObservabilityAIAssistantAppService | undefined; + appService: AIAssistantAppService | undefined; constructor(context: PluginInitializerContext) { this.logger = context.logger.get(); diff --git a/x-pack/plugins/observability_solution/observability_ai_assistant_app/public/routes/config.tsx b/x-pack/plugins/observability_solution/observability_ai_assistant_app/public/routes/config.tsx index ed0ac18302cc5..545c69a990ace 100644 --- a/x-pack/plugins/observability_solution/observability_ai_assistant_app/public/routes/config.tsx +++ b/x-pack/plugins/observability_solution/observability_ai_assistant_app/public/routes/config.tsx @@ -9,8 +9,8 @@ import { createRouter, Outlet } from '@kbn/typed-react-router-config'; import * as t from 'io-ts'; import React from 'react'; import { Redirect } from 'react-router-dom'; +import { ConversationViewWithProps } from './conversations/conversation_view_with_props'; import { ObservabilityAIAssistantPageTemplate } from '../components/page_template'; -import { ConversationView } from './conversations/conversation_view'; /** * The array of route definitions to be used when the application @@ -28,7 +28,7 @@ const observabilityAIAssistantRoutes = { ), children: { '/conversations/new': { - element: , + element: , }, '/conversations/{conversationId}': { params: t.intersection([ @@ -43,7 +43,7 @@ const observabilityAIAssistantRoutes = { }), }), ]), - element: , + element: , }, '/conversations': { element: , diff --git a/x-pack/plugins/observability_solution/observability_ai_assistant_app/public/routes/conversations/conversation_view_with_props.tsx b/x-pack/plugins/observability_solution/observability_ai_assistant_app/public/routes/conversations/conversation_view_with_props.tsx new file mode 100644 index 0000000000000..c57b8e2c66c71 --- /dev/null +++ b/x-pack/plugins/observability_solution/observability_ai_assistant_app/public/routes/conversations/conversation_view_with_props.tsx @@ -0,0 +1,43 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React from 'react'; +import { ConversationView } from '@kbn/ai-assistant'; +import { useObservabilityAIAssistantParams } from '../../hooks/use_observability_ai_assistant_params'; +import { useObservabilityAIAssistantRouter } from '../../hooks/use_observability_ai_assistant_router'; + +export function ConversationViewWithProps() { + const { path } = useObservabilityAIAssistantParams('/conversations/*'); + const conversationId = 'conversationId' in path ? path.conversationId : undefined; + const observabilityAIAssistantRouter = useObservabilityAIAssistantRouter(); + function navigateToConversation(nextConversationId?: string) { + if (nextConversationId) { + observabilityAIAssistantRouter.push('/conversations/{conversationId}', { + path: { + conversationId: nextConversationId, + }, + query: {}, + }); + } else { + observabilityAIAssistantRouter.push('/conversations/new', { path: {}, query: {} }); + } + } + return ( + + observabilityAIAssistantRouter.link(`/conversations/{conversationId}`, { + path: { + conversationId: id, + }, + }) + } + /> + ); +} diff --git a/x-pack/plugins/observability_solution/observability_ai_assistant_app/public/utils/shared_providers.tsx b/x-pack/plugins/observability_solution/observability_ai_assistant_app/public/utils/shared_providers.tsx index eaa441b34a008..49776f4622250 100644 --- a/x-pack/plugins/observability_solution/observability_ai_assistant_app/public/utils/shared_providers.tsx +++ b/x-pack/plugins/observability_solution/observability_ai_assistant_app/public/utils/shared_providers.tsx @@ -11,8 +11,7 @@ import { KibanaThemeProvider } from '@kbn/react-kibana-context-theme'; import { RedirectAppLinks } from '@kbn/shared-ux-link-redirect-app'; import React, { useMemo } from 'react'; import type { Observable } from 'rxjs'; -import { ObservabilityAIAssistantAppServiceProvider } from '../context/observability_ai_assistant_app_service_provider'; -import type { ObservabilityAIAssistantAppService } from '../service/create_app_service'; +import { AIAssistantAppService } from '@kbn/ai-assistant'; import type { ObservabilityAIAssistantAppPluginStartDependencies } from '../types'; export function SharedProviders({ @@ -25,7 +24,7 @@ export function SharedProviders({ children: React.ReactElement; coreStart: CoreStart; pluginsStart: ObservabilityAIAssistantAppPluginStartDependencies; - service: ObservabilityAIAssistantAppService; + service: AIAssistantAppService; theme$: Observable; }) { const theme = useMemo(() => { @@ -45,11 +44,7 @@ export function SharedProviders({ }} > - - - {children} - - + {children} diff --git a/x-pack/plugins/observability_solution/observability_ai_assistant_app/tsconfig.json b/x-pack/plugins/observability_solution/observability_ai_assistant_app/tsconfig.json index 84fe8f0b93911..f5b6d1db53885 100644 --- a/x-pack/plugins/observability_solution/observability_ai_assistant_app/tsconfig.json +++ b/x-pack/plugins/observability_solution/observability_ai_assistant_app/tsconfig.json @@ -20,16 +20,8 @@ "@kbn/typed-react-router-config", "@kbn/i18n", "@kbn/management-settings-ids", - "@kbn/security-plugin", - "@kbn/ui-theme", - "@kbn/actions-plugin", - "@kbn/user-profile-components", - "@kbn/core-http-browser", "@kbn/triggers-actions-ui-plugin", "@kbn/shared-ux-utility", - "@kbn/i18n-react", - "@kbn/code-editor", - "@kbn/monaco", "@kbn/data-views-plugin", "@kbn/lens-embeddable-utils", "@kbn/lens-plugin", @@ -38,7 +30,6 @@ "@kbn/esql-utils", "@kbn/visualization-utils", "@kbn/ai-assistant-management-plugin", - "@kbn/utility-types-jest", "@kbn/kibana-react-plugin", "@kbn/licensing-plugin", "@kbn/logging", @@ -56,6 +47,11 @@ "@kbn/apm-synthtrace-client", "@kbn/alerting-plugin", "@kbn/apm-synthtrace", + "@kbn/esql-datagrid", + "@kbn/alerting-comparators", + "@kbn/core-lifecycle-browser", + "@kbn/inference-plugin", + "@kbn/ai-assistant", "@kbn/apm-utils", "@kbn/config-schema", "@kbn/es-query", @@ -63,17 +59,17 @@ "@kbn/esql-validation-autocomplete", "@kbn/esql-ast", "@kbn/field-types", + "@kbn/security-plugin", + "@kbn/observability-plugin", + "@kbn/actions-plugin", "@kbn/stack-connectors-plugin", "@kbn/features-plugin", "@kbn/serverless", "@kbn/task-manager-plugin", "@kbn/cloud-plugin", - "@kbn/observability-plugin", - "@kbn/esql-datagrid", - "@kbn/alerting-comparators", - "@kbn/core-lifecycle-browser", - "@kbn/inference-plugin", - "@kbn/logs-data-access-plugin" + "@kbn/logs-data-access-plugin", ], - "exclude": ["target/**/*"] + "exclude": [ + "target/**/*" + ] } diff --git a/x-pack/plugins/observability_solution/observability_shared/common/field_names/elasticsearch.ts b/x-pack/plugins/observability_solution/observability_shared/common/field_names/elasticsearch.ts index 741e9b6b0e2d3..5652671a87281 100644 --- a/x-pack/plugins/observability_solution/observability_shared/common/field_names/elasticsearch.ts +++ b/x-pack/plugins/observability_solution/observability_shared/common/field_names/elasticsearch.ts @@ -153,4 +153,5 @@ export const ENTITY_LAST_SEEN = 'entity.lastSeenTimestamp'; export const ENTITY_FIRST_SEEN = 'entity.firstSeenTimestamp'; export const ENTITY_DISPLAY_NAME = 'entity.displayName'; export const ENTITY_DEFINITION_ID = 'entity.definitionId'; +export const ENTITY_IDENTITY_FIELDS = 'entity.identityFields'; export const SOURCE_DATA_STREAM_TYPE = 'source_data_stream.type'; diff --git a/x-pack/plugins/observability_solution/observability_shared/common/index.ts b/x-pack/plugins/observability_solution/observability_shared/common/index.ts index a33c0aa99d1e2..a359a3d862ce9 100644 --- a/x-pack/plugins/observability_solution/observability_shared/common/index.ts +++ b/x-pack/plugins/observability_solution/observability_shared/common/index.ts @@ -136,6 +136,7 @@ export { ENTITY_LAST_SEEN, ENTITY_TYPE, SOURCE_DATA_STREAM_TYPE, + ENTITY_IDENTITY_FIELDS, } from './field_names/elasticsearch'; export { diff --git a/x-pack/plugins/observability_solution/synthetics/server/synthetics_service/service_api_client.ts b/x-pack/plugins/observability_solution/synthetics/server/synthetics_service/service_api_client.ts index 3d334f32e9407..73f286e40d310 100644 --- a/x-pack/plugins/observability_solution/synthetics/server/synthetics_service/service_api_client.ts +++ b/x-pack/plugins/observability_solution/synthetics/server/synthetics_service/service_api_client.ts @@ -143,6 +143,10 @@ export class ServiceAPIClient { cert: tlsConfig.certificate, key: tlsConfig.key, }); + } else if (!this.server.isDev) { + this.logger.warn( + 'TLS certificate and key are not provided. Falling back to default HTTPS agent.' + ); } return baseHttpsAgent; diff --git a/x-pack/plugins/search_assistant/kibana.jsonc b/x-pack/plugins/search_assistant/kibana.jsonc index 85579b76a1e80..8391ee14e0d88 100644 --- a/x-pack/plugins/search_assistant/kibana.jsonc +++ b/x-pack/plugins/search_assistant/kibana.jsonc @@ -12,13 +12,19 @@ "searchAssistant" ], "requiredPlugins": [ + "actions", + "licensing", "observabilityAIAssistant", - "observabilityAIAssistantApp" + "observabilityAIAssistantApp", + "triggersActionsUi", + "share" ], "optionalPlugins": [ "cloud", "usageCollection", ], - "requiredBundles": [] + "requiredBundles": [ + "kibanaReact" + ] } } diff --git a/x-pack/plugins/search_assistant/public/application.tsx b/x-pack/plugins/search_assistant/public/application.tsx index 071c51f4b6e13..1bbf7063ec373 100644 --- a/x-pack/plugins/search_assistant/public/application.tsx +++ b/x-pack/plugins/search_assistant/public/application.tsx @@ -7,31 +7,28 @@ import React from 'react'; import ReactDOM from 'react-dom'; -import type { CoreStart } from '@kbn/core/public'; +import type { AppMountParameters, CoreStart } from '@kbn/core/public'; import { KibanaRenderContextProvider } from '@kbn/react-kibana-context-render'; import { KibanaContextProvider } from '@kbn/kibana-react-plugin/public'; import { I18nProvider } from '@kbn/i18n-react'; -import { Router } from '@kbn/shared-ux-router'; import type { SearchAssistantPluginStartDependencies } from './types'; -import { SearchAssistantRouter } from './router'; +import { SearchAssistantRouter } from './components/routes/router'; export const renderApp = ( core: CoreStart, services: SearchAssistantPluginStartDependencies, - element: HTMLElement + appMountParameters: AppMountParameters ) => { ReactDOM.render( - - - + , - element + appMountParameters.element ); - return () => ReactDOM.unmountComponentAtNode(element); + return () => ReactDOM.unmountComponentAtNode(appMountParameters.element); }; diff --git a/x-pack/plugins/search_assistant/public/components/page_template.tsx b/x-pack/plugins/search_assistant/public/components/page_template.tsx new file mode 100644 index 0000000000000..e9fb3a45e9e2b --- /dev/null +++ b/x-pack/plugins/search_assistant/public/components/page_template.tsx @@ -0,0 +1,12 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ +import React from 'react'; +import { KibanaPageTemplate } from '@kbn/shared-ux-page-kibana-template'; + +export function SearchAIAssistantPageTemplate({ children }: { children: React.ReactNode }) { + return {children}; +} diff --git a/x-pack/plugins/search_assistant/public/components/routes/conversations/conversation_view_with_props.tsx b/x-pack/plugins/search_assistant/public/components/routes/conversations/conversation_view_with_props.tsx new file mode 100644 index 0000000000000..545ff1ceb7370 --- /dev/null +++ b/x-pack/plugins/search_assistant/public/components/routes/conversations/conversation_view_with_props.tsx @@ -0,0 +1,35 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React from 'react'; +import { ConversationView } from '@kbn/ai-assistant'; +import { useParams } from 'react-router-dom'; +import { useKibana } from '@kbn/kibana-react-plugin/public'; + +export function ConversationViewWithProps() { + const { conversationId } = useParams<{ conversationId?: string }>(); + const { + services: { application, http }, + } = useKibana(); + function navigateToConversation(nextConversationId?: string) { + application?.navigateToUrl( + http?.basePath.prepend(`/app/searchAssistant/conversations/${nextConversationId || ''}`) || '' + ); + } + return ( + + http?.basePath.prepend(`/app/searchAssistant/conversations/${id || ''}`) || '' + } + /> + ); +} diff --git a/x-pack/plugins/search_assistant/public/components/routes/router.tsx b/x-pack/plugins/search_assistant/public/components/routes/router.tsx new file mode 100644 index 0000000000000..154bc2ab46a3e --- /dev/null +++ b/x-pack/plugins/search_assistant/public/components/routes/router.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 from 'react'; +import { History } from 'history'; +import { Route, Router, Routes } from '@kbn/shared-ux-router'; +import { Redirect } from 'react-router-dom'; +import { SearchAIAssistantPageTemplate } from '../page_template'; +import { ConversationViewWithProps } from './conversations/conversation_view_with_props'; + +export const SearchAssistantRouter: React.FC<{ history: History }> = ({ history }) => { + return ( + + + + + + + + + + + + + + + ); +}; diff --git a/x-pack/plugins/search_assistant/public/components/search_assistant.tsx b/x-pack/plugins/search_assistant/public/components/search_assistant.tsx deleted file mode 100644 index 9c227a4e7b73f..0000000000000 --- a/x-pack/plugins/search_assistant/public/components/search_assistant.tsx +++ /dev/null @@ -1,24 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { EuiPageTemplate } from '@elastic/eui'; -import React from 'react'; -import { App } from './app'; - -export const SearchAssistantPage: React.FC = () => { - return ( - - - - ); -}; diff --git a/x-pack/plugins/search_assistant/public/index.ts b/x-pack/plugins/search_assistant/public/index.ts index c2b16e857b53e..cb84f8519fd96 100644 --- a/x-pack/plugins/search_assistant/public/index.ts +++ b/x-pack/plugins/search_assistant/public/index.ts @@ -5,9 +5,19 @@ * 2.0. */ -import { SearchAssistantPlugin } from './plugin'; +import { PluginInitializer, PluginInitializerContext } from '@kbn/core/public'; +import { PublicConfigType, SearchAssistantPlugin } from './plugin'; +import { + SearchAssistantPluginSetup, + SearchAssistantPluginStart, + SearchAssistantPluginStartDependencies, +} from './types'; + +export const plugin: PluginInitializer< + SearchAssistantPluginSetup, + SearchAssistantPluginStart, + {}, + SearchAssistantPluginStartDependencies +> = (context: PluginInitializerContext) => new SearchAssistantPlugin(context); -export function plugin() { - return new SearchAssistantPlugin(); -} export type { SearchAssistantPluginSetup, SearchAssistantPluginStart } from './types'; diff --git a/x-pack/plugins/search_assistant/public/plugin.ts b/x-pack/plugins/search_assistant/public/plugin.ts index 8ba22a48df9ff..1c09502c154ad 100644 --- a/x-pack/plugins/search_assistant/public/plugin.ts +++ b/x-pack/plugins/search_assistant/public/plugin.ts @@ -5,19 +5,71 @@ * 2.0. */ -import type { CoreSetup, Plugin } from '@kbn/core/public'; +import { + DEFAULT_APP_CATEGORIES, + type CoreSetup, + type Plugin, + CoreStart, + AppMountParameters, + PluginInitializerContext, +} from '@kbn/core/public'; +import { i18n } from '@kbn/i18n'; import type { SearchAssistantPluginSetup, SearchAssistantPluginStart, SearchAssistantPluginStartDependencies, } from './types'; +export interface PublicConfigType { + ui: { + enabled: boolean; + }; +} + export class SearchAssistantPlugin - implements Plugin + implements + Plugin< + SearchAssistantPluginSetup, + SearchAssistantPluginStart, + {}, + SearchAssistantPluginStartDependencies + > { + private readonly config: PublicConfigType; + + constructor(private readonly context: PluginInitializerContext) { + this.config = this.context.config.get(); + } + public setup( core: CoreSetup ): SearchAssistantPluginSetup { + if (!this.config.ui.enabled) { + return {}; + } + + core.application.register({ + id: 'searchAssistant', + title: i18n.translate('xpack.searchAssistant.appTitle', { + defaultMessage: 'Search Assistant', + }), + euiIconType: 'logoEnterpriseSearch', + appRoute: '/app/searchAssistant', + category: DEFAULT_APP_CATEGORIES.search, + visibleIn: [], + deepLinks: [], + mount: async (appMountParameters: AppMountParameters) => { + // Load application bundle and Get start services + const [{ renderApp }, [coreStart, pluginsStart]] = await Promise.all([ + import('./application'), + core.getStartServices() as Promise< + [CoreStart, SearchAssistantPluginStartDependencies, unknown] + >, + ]); + + return renderApp(coreStart, pluginsStart, appMountParameters); + }, + }); return {}; } diff --git a/x-pack/plugins/search_assistant/public/router.tsx b/x-pack/plugins/search_assistant/public/router.tsx deleted file mode 100644 index a25f865b4f74a..0000000000000 --- a/x-pack/plugins/search_assistant/public/router.tsx +++ /dev/null @@ -1,20 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { Route, Routes } from '@kbn/shared-ux-router'; -import React from 'react'; -import { SearchAssistantPage } from './components/search_assistant'; - -export const SearchAssistantRouter: React.FC = () => { - return ( - - - - - - ); -}; diff --git a/x-pack/plugins/search_assistant/public/types.ts b/x-pack/plugins/search_assistant/public/types.ts index f05592414a9dc..b1a5d6164b1f1 100644 --- a/x-pack/plugins/search_assistant/public/types.ts +++ b/x-pack/plugins/search_assistant/public/types.ts @@ -5,7 +5,6 @@ * 2.0. */ -import { AppMountParameters } from '@kbn/core/public'; import { UsageCollectionStart } from '@kbn/usage-collection-plugin/public'; import { ObservabilityAIAssistantPublicStart } from '@kbn/observability-ai-assistant-plugin/public'; @@ -16,7 +15,6 @@ export interface SearchAssistantPluginSetup {} export interface SearchAssistantPluginStart {} export interface SearchAssistantPluginStartDependencies { - history: AppMountParameters['history']; observabilityAIAssistant: ObservabilityAIAssistantPublicStart; usageCollection?: UsageCollectionStart; } diff --git a/x-pack/plugins/search_assistant/server/config.ts b/x-pack/plugins/search_assistant/server/config.ts index a09b7ac51b7b7..5ca081ec8a667 100644 --- a/x-pack/plugins/search_assistant/server/config.ts +++ b/x-pack/plugins/search_assistant/server/config.ts @@ -9,11 +9,19 @@ import { schema, TypeOf } from '@kbn/config-schema'; import { PluginConfigDescriptor } from '@kbn/core/server'; const configSchema = schema.object({ - enabled: schema.boolean({ defaultValue: false }), + enabled: schema.boolean({ defaultValue: true }), + ui: schema.object({ + enabled: schema.boolean({ defaultValue: false }), + }), }); export type SearchAssistantConfig = TypeOf; export const config: PluginConfigDescriptor = { + exposeToBrowser: { + ui: { + enabled: true, + }, + }, schema: configSchema, }; diff --git a/x-pack/plugins/search_assistant/tsconfig.json b/x-pack/plugins/search_assistant/tsconfig.json index 090356cf1f440..d865d2bdbff83 100644 --- a/x-pack/plugins/search_assistant/tsconfig.json +++ b/x-pack/plugins/search_assistant/tsconfig.json @@ -16,11 +16,13 @@ "@kbn/react-kibana-context-render", "@kbn/kibana-react-plugin", "@kbn/i18n-react", - "@kbn/shared-ux-router", "@kbn/shared-ux-page-kibana-template", "@kbn/usage-collection-plugin", "@kbn/observability-ai-assistant-plugin", - "@kbn/config-schema" + "@kbn/config-schema", + "@kbn/ai-assistant", + "@kbn/i18n", + "@kbn/shared-ux-router" ], "exclude": [ "target/**/*", diff --git a/x-pack/plugins/search_playground/common/types.ts b/x-pack/plugins/search_playground/common/types.ts index c239858b5b459..e2a0ae34c2ef3 100644 --- a/x-pack/plugins/search_playground/common/types.ts +++ b/x-pack/plugins/search_playground/common/types.ts @@ -57,6 +57,7 @@ export enum APIRoutes { export enum LLMs { openai = 'openai', openai_azure = 'openai_azure', + openai_other = 'openai_other', bedrock = 'bedrock', gemini = 'gemini', } diff --git a/x-pack/plugins/search_playground/public/hooks/use_llms_models.test.ts b/x-pack/plugins/search_playground/public/hooks/use_llms_models.test.ts index d661084306583..ebce3883a471b 100644 --- a/x-pack/plugins/search_playground/public/hooks/use_llms_models.test.ts +++ b/x-pack/plugins/search_playground/public/hooks/use_llms_models.test.ts @@ -15,9 +15,10 @@ jest.mock('./use_load_connectors', () => ({ })); const mockConnectors = [ - { id: 'connectorId1', title: 'OpenAI Connector', type: LLMs.openai }, - { id: 'connectorId2', title: 'OpenAI Azure Connector', type: LLMs.openai_azure }, - { id: 'connectorId2', title: 'Bedrock Connector', type: LLMs.bedrock }, + { id: 'connectorId1', name: 'OpenAI Connector', type: LLMs.openai }, + { id: 'connectorId2', name: 'OpenAI Azure Connector', type: LLMs.openai_azure }, + { id: 'connectorId2', name: 'Bedrock Connector', type: LLMs.bedrock }, + { id: 'connectorId3', name: 'OpenAI OSS Model Connector', type: LLMs.openai_other }, ]; const mockUseLoadConnectors = (data: any) => { (useLoadConnectors as jest.Mock).mockReturnValue({ data }); @@ -36,7 +37,7 @@ describe('useLLMsModels Hook', () => { expect(result.current).toEqual([ { connectorId: 'connectorId1', - connectorName: undefined, + connectorName: 'OpenAI Connector', connectorType: LLMs.openai, disabled: false, icon: expect.any(Function), @@ -48,7 +49,7 @@ describe('useLLMsModels Hook', () => { }, { connectorId: 'connectorId1', - connectorName: undefined, + connectorName: 'OpenAI Connector', connectorType: LLMs.openai, disabled: false, icon: expect.any(Function), @@ -60,7 +61,7 @@ describe('useLLMsModels Hook', () => { }, { connectorId: 'connectorId1', - connectorName: undefined, + connectorName: 'OpenAI Connector', connectorType: LLMs.openai, disabled: false, icon: expect.any(Function), @@ -72,19 +73,19 @@ describe('useLLMsModels Hook', () => { }, { connectorId: 'connectorId2', - connectorName: undefined, + connectorName: 'OpenAI Azure Connector', connectorType: LLMs.openai_azure, disabled: false, icon: expect.any(Function), - id: 'connectorId2Azure OpenAI ', - name: 'Azure OpenAI ', + id: 'connectorId2OpenAI Azure Connector (Azure OpenAI)', + name: 'OpenAI Azure Connector (Azure OpenAI)', showConnectorName: false, value: undefined, promptTokenLimit: undefined, }, { connectorId: 'connectorId2', - connectorName: undefined, + connectorName: 'Bedrock Connector', connectorType: LLMs.bedrock, disabled: false, icon: expect.any(Function), @@ -96,7 +97,7 @@ describe('useLLMsModels Hook', () => { }, { connectorId: 'connectorId2', - connectorName: undefined, + connectorName: 'Bedrock Connector', connectorType: LLMs.bedrock, disabled: false, icon: expect.any(Function), @@ -106,6 +107,18 @@ describe('useLLMsModels Hook', () => { value: 'anthropic.claude-3-5-sonnet-20240620-v1:0', promptTokenLimit: 200000, }, + { + connectorId: 'connectorId3', + connectorName: 'OpenAI OSS Model Connector', + connectorType: LLMs.openai_other, + disabled: false, + icon: expect.any(Function), + id: 'connectorId3OpenAI OSS Model Connector (OpenAI Compatible Service)', + name: 'OpenAI OSS Model Connector (OpenAI Compatible Service)', + showConnectorName: false, + value: undefined, + promptTokenLimit: undefined, + }, ]); }); diff --git a/x-pack/plugins/search_playground/public/hooks/use_llms_models.ts b/x-pack/plugins/search_playground/public/hooks/use_llms_models.ts index 7a9b01e085a6d..3d5cee7719f10 100644 --- a/x-pack/plugins/search_playground/public/hooks/use_llms_models.ts +++ b/x-pack/plugins/search_playground/public/hooks/use_llms_models.ts @@ -34,11 +34,22 @@ const mapLlmToModels: Record< }, [LLMs.openai_azure]: { icon: OpenAILogo, - getModels: (connectorName, includeName) => [ + getModels: (connectorName) => [ { label: i18n.translate('xpack.searchPlayground.openAIAzureModel', { - defaultMessage: 'Azure OpenAI {name}', - values: { name: includeName ? `(${connectorName})` : '' }, + defaultMessage: '{name} (Azure OpenAI)', + values: { name: connectorName }, + }), + }, + ], + }, + [LLMs.openai_other]: { + icon: OpenAILogo, + getModels: (connectorName) => [ + { + label: i18n.translate('xpack.searchPlayground.otherOpenAIModel', { + defaultMessage: '{name} (OpenAI Compatible Service)', + values: { name: connectorName }, }), }, ], diff --git a/x-pack/plugins/search_playground/public/hooks/use_load_connectors.test.ts b/x-pack/plugins/search_playground/public/hooks/use_load_connectors.test.ts index 3a68d91fd0246..eb2f36eb62e5f 100644 --- a/x-pack/plugins/search_playground/public/hooks/use_load_connectors.test.ts +++ b/x-pack/plugins/search_playground/public/hooks/use_load_connectors.test.ts @@ -71,6 +71,12 @@ describe('useLoadConnectors', () => { actionTypeId: '.bedrock', isMissingSecrets: false, }, + { + id: '5', + actionTypeId: '.gen-ai', + isMissingSecrets: false, + config: { apiProvider: OpenAiProviderType.Other }, + }, ]; mockedLoadConnectors.mockResolvedValue(connectors); @@ -106,6 +112,16 @@ describe('useLoadConnectors', () => { title: 'Bedrock', type: 'bedrock', }, + { + actionTypeId: '.gen-ai', + config: { + apiProvider: 'Other', + }, + id: '5', + isMissingSecrets: false, + title: 'OpenAI Other', + type: 'openai_other', + }, ]); }); }); diff --git a/x-pack/plugins/search_playground/public/hooks/use_load_connectors.ts b/x-pack/plugins/search_playground/public/hooks/use_load_connectors.ts index 94bb2da37b1ed..3d2a3e8c90b86 100644 --- a/x-pack/plugins/search_playground/public/hooks/use_load_connectors.ts +++ b/x-pack/plugins/search_playground/public/hooks/use_load_connectors.ts @@ -63,6 +63,20 @@ const connectorTypeToLLM: Array<{ type: LLMs.openai, }), }, + { + actionId: OPENAI_CONNECTOR_ID, + actionProvider: OpenAiProviderType.Other, + match: (connector) => + connector.actionTypeId === OPENAI_CONNECTOR_ID && + (connector as OpenAIConnector)?.config?.apiProvider === OpenAiProviderType.Other, + transform: (connector) => ({ + ...connector, + title: i18n.translate('xpack.searchPlayground.openAIOtherConnectorTitle', { + defaultMessage: 'OpenAI Other', + }), + type: LLMs.openai_other, + }), + }, { actionId: BEDROCK_CONNECTOR_ID, match: (connector) => connector.actionTypeId === BEDROCK_CONNECTOR_ID, diff --git a/x-pack/plugins/search_playground/server/lib/get_chat_params.test.ts b/x-pack/plugins/search_playground/server/lib/get_chat_params.test.ts index cbc696a50085e..614d00dc16e66 100644 --- a/x-pack/plugins/search_playground/server/lib/get_chat_params.test.ts +++ b/x-pack/plugins/search_playground/server/lib/get_chat_params.test.ts @@ -152,4 +152,41 @@ describe('getChatParams', () => { ) ).rejects.toThrow('Invalid connector id'); }); + + it('returns the correct chat model and uses the default model when not specified in the params', async () => { + mockActionsClient.get.mockResolvedValue({ + id: '2', + actionTypeId: OPENAI_CONNECTOR_ID, + config: { defaultModel: 'local' }, + }); + + const result = await getChatParams( + { + connectorId: '2', + prompt: 'How does it work?', + citations: false, + }, + { actions, request, logger } + ); + + expect(Prompt).toHaveBeenCalledWith('How does it work?', { + citations: false, + context: true, + type: 'openai', + }); + expect(QuestionRewritePrompt).toHaveBeenCalledWith({ + type: 'openai', + }); + expect(ActionsClientChatOpenAI).toHaveBeenCalledWith({ + logger: expect.anything(), + model: 'local', + connectorId: '2', + actionsClient: expect.anything(), + signal: expect.anything(), + traceId: 'test-uuid', + temperature: 0.2, + maxRetries: 0, + }); + expect(result.chatPrompt).toContain('How does it work?'); + }); }); diff --git a/x-pack/plugins/search_playground/server/lib/get_chat_params.ts b/x-pack/plugins/search_playground/server/lib/get_chat_params.ts index d2c4bb1afaa9d..34f902e0d1ca2 100644 --- a/x-pack/plugins/search_playground/server/lib/get_chat_params.ts +++ b/x-pack/plugins/search_playground/server/lib/get_chat_params.ts @@ -57,7 +57,7 @@ export const getChatParams = async ( actionsClient, logger, connectorId, - model, + model: model || connector?.config?.defaultModel, traceId: uuidv4(), signal: abortSignal, temperature: getDefaultArguments().temperature, diff --git a/x-pack/plugins/searchprofiler/public/application/components/_index.scss b/x-pack/plugins/searchprofiler/public/application/components/_index.scss index 9d6688a2d4d98..ee36c5e8e6567 100644 --- a/x-pack/plugins/searchprofiler/public/application/components/_index.scss +++ b/x-pack/plugins/searchprofiler/public/application/components/_index.scss @@ -3,5 +3,4 @@ $badgeSize: $euiSize * 5.5; @import 'highlight_details_flyout/highlight_details_flyout'; @import 'license_warning_notice/license_warning_notice'; @import 'percentage_badge/percentage_badge'; -@import 'profile_query_editor/profile_query_editor'; @import 'profile_tree/index'; diff --git a/x-pack/plugins/searchprofiler/public/application/components/profile_query_editor/_profile_query_editor.scss b/x-pack/plugins/searchprofiler/public/application/components/profile_query_editor/_profile_query_editor.scss deleted file mode 100644 index 035ff16c990bb..0000000000000 --- a/x-pack/plugins/searchprofiler/public/application/components/profile_query_editor/_profile_query_editor.scss +++ /dev/null @@ -1,25 +0,0 @@ - -.prfDevTool__sense { - order: 1; - // To anchor ace editor - position: relative; - - // Ace Editor overrides - .ace_editor { - min-height: $euiSize * 10; - flex-grow: 1; - margin-bottom: $euiSize; - margin-top: $euiSize; - outline: solid 1px $euiColorLightShade; - } - - .errorMarker { - position: absolute; - background: rgba($euiColorDanger, .5); - z-index: 20; - } -} - -.prfDevTool__profileButtonContainer { - flex-shrink: 1; -} diff --git a/x-pack/plugins/searchprofiler/public/application/components/profile_query_editor/editor/editor.test.tsx b/x-pack/plugins/searchprofiler/public/application/components/profile_query_editor/editor/editor.test.tsx index 34e0867df8ec6..483f0ef7f6106 100644 --- a/x-pack/plugins/searchprofiler/public/application/components/profile_query_editor/editor/editor.test.tsx +++ b/x-pack/plugins/searchprofiler/public/application/components/profile_query_editor/editor/editor.test.tsx @@ -5,20 +5,14 @@ * 2.0. */ -import 'brace'; -import 'brace/mode/json'; - -import { coreMock } from '@kbn/core/public/mocks'; import { registerTestBed } from '@kbn/test-jest-helpers'; import { Editor, Props } from './editor'; -const coreStart = coreMock.createStart(); - describe('Editor Component', () => { it('renders', async () => { const props: Props = { - ...coreStart, - initialValue: '', + editorValue: '', + setEditorValue: () => {}, licenseEnabled: true, onEditorReady: (e: any) => {}, }; diff --git a/x-pack/plugins/searchprofiler/public/application/components/profile_query_editor/editor/editor.tsx b/x-pack/plugins/searchprofiler/public/application/components/profile_query_editor/editor/editor.tsx index 068673d4ce4c1..3701323d414c2 100644 --- a/x-pack/plugins/searchprofiler/public/application/components/profile_query_editor/editor/editor.tsx +++ b/x-pack/plugins/searchprofiler/public/application/components/profile_query_editor/editor/editor.tsx @@ -5,67 +5,37 @@ * 2.0. */ -import React, { memo, useRef, useEffect, useState } from 'react'; +import React, { memo, useCallback } from 'react'; import { i18n } from '@kbn/i18n'; -import { EuiScreenReaderOnly } from '@elastic/eui'; -import { Editor as AceEditor } from 'brace'; +import { EuiScreenReaderOnly, EuiSpacer } from '@elastic/eui'; +import { CodeEditor } from '@kbn/code-editor'; +import { monaco, XJsonLang } from '@kbn/monaco'; -import { SearchProfilerStartServices } from '../../../../types'; -import { ace } from '../../../../shared_imports'; -import { initializeEditor } from './init_editor'; - -const { useUIAceKeyboardMode } = ace; - -type EditorShim = ReturnType; - -export type EditorInstance = EditorShim; - -export interface Props extends SearchProfilerStartServices { +export interface Props { licenseEnabled: boolean; - initialValue: string; - onEditorReady: (editor: EditorShim) => void; + editorValue: string; + setEditorValue: (value: string) => void; + onEditorReady: (props: EditorProps) => void; } -const createEditorShim = (aceEditor: AceEditor) => { - return { - getValue() { - return aceEditor.getValue(); - }, - focus() { - aceEditor.focus(); - }, - }; -}; - const EDITOR_INPUT_ID = 'SearchProfilerTextArea'; -export const Editor = memo( - ({ licenseEnabled, initialValue, onEditorReady, ...startServices }: Props) => { - const containerRef = useRef(null as any); - const editorInstanceRef = useRef(null as any); - - const [textArea, setTextArea] = useState(null); - - useUIAceKeyboardMode(textArea, startServices); - - useEffect(() => { - const divEl = containerRef.current; - editorInstanceRef.current = initializeEditor({ el: divEl, licenseEnabled }); - editorInstanceRef.current.setValue(initialValue, 1); - const textarea = divEl.querySelector('textarea'); - if (textarea) { - textarea.setAttribute('id', EDITOR_INPUT_ID); - } - setTextArea(licenseEnabled ? containerRef.current!.querySelector('textarea') : null); - - onEditorReady(createEditorShim(editorInstanceRef.current)); +export interface EditorProps { + focus: () => void; +} - return () => { - if (editorInstanceRef.current) { - editorInstanceRef.current.destroy(); - } - }; - }, [initialValue, onEditorReady, licenseEnabled]); +export const Editor = memo( + ({ licenseEnabled, editorValue, setEditorValue, onEditorReady }: Props) => { + const editorDidMountCallback = useCallback( + (editor: monaco.editor.IStandaloneCodeEditor) => { + onEditorReady({ + focus: () => { + editor.focus(); + }, + } as EditorProps); + }, + [onEditorReady] + ); return ( <> @@ -76,7 +46,26 @@ export const Editor = memo( })} -
    + + + + ); } diff --git a/x-pack/plugins/searchprofiler/public/application/components/profile_query_editor/editor/index.ts b/x-pack/plugins/searchprofiler/public/application/components/profile_query_editor/editor/index.ts index 5d8be48041176..1ac3ec704bc5d 100644 --- a/x-pack/plugins/searchprofiler/public/application/components/profile_query_editor/editor/index.ts +++ b/x-pack/plugins/searchprofiler/public/application/components/profile_query_editor/editor/index.ts @@ -5,5 +5,4 @@ * 2.0. */ -export type { EditorInstance } from './editor'; -export { Editor } from './editor'; +export { Editor, type EditorProps } from './editor'; diff --git a/x-pack/plugins/searchprofiler/public/application/components/profile_query_editor/editor/init_editor.ts b/x-pack/plugins/searchprofiler/public/application/components/profile_query_editor/editor/init_editor.ts deleted file mode 100644 index 24d119254db78..0000000000000 --- a/x-pack/plugins/searchprofiler/public/application/components/profile_query_editor/editor/init_editor.ts +++ /dev/null @@ -1,36 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import ace from 'brace'; -import { installXJsonMode } from '@kbn/ace'; - -export function initializeEditor({ - el, - licenseEnabled, -}: { - el: HTMLDivElement; - licenseEnabled: boolean; -}) { - const editor: ace.Editor = ace.acequire('ace/ace').edit(el); - - installXJsonMode(editor); - editor.$blockScrolling = Infinity; - - if (!licenseEnabled) { - editor.setReadOnly(true); - editor.container.style.pointerEvents = 'none'; - editor.container.style.opacity = '0.5'; - const textArea = editor.container.querySelector('textarea'); - if (textArea) { - textArea.setAttribute('tabindex', '-1'); - } - editor.renderer.setStyle('disabled'); - editor.blur(); - } - - return editor; -} diff --git a/x-pack/plugins/searchprofiler/public/application/components/profile_query_editor/profile_query_editor.tsx b/x-pack/plugins/searchprofiler/public/application/components/profile_query_editor/profile_query_editor.tsx index 577c3e530e8cc..a88f1040caa3a 100644 --- a/x-pack/plugins/searchprofiler/public/application/components/profile_query_editor/profile_query_editor.tsx +++ b/x-pack/plugins/searchprofiler/public/application/components/profile_query_editor/profile_query_editor.tsx @@ -5,7 +5,7 @@ * 2.0. */ -import React, { useRef, memo, useCallback } from 'react'; +import React, { useRef, memo, useCallback, useState } from 'react'; import { i18n } from '@kbn/i18n'; import { EuiForm, @@ -23,7 +23,7 @@ import { decompressFromEncodedURIComponent } from 'lz-string'; import { useRequestProfile } from '../../hooks'; import { useAppContext } from '../../contexts/app_context'; import { useProfilerActionContext } from '../../contexts/profiler_context'; -import { Editor, EditorInstance } from './editor'; +import { Editor, type EditorProps } from './editor'; const DEFAULT_INDEX_VALUE = '_all'; @@ -39,33 +39,36 @@ const INITIAL_EDITOR_VALUE = `{ * Drives state changes for mine via profiler action context. */ export const ProfileQueryEditor = memo(() => { - const editorRef = useRef(null as any); + const editorPropsRef = useRef(null as any); const indexInputRef = useRef(null as any); const dispatch = useProfilerActionContext(); - const { getLicenseStatus, notifications, location, ...startServices } = useAppContext(); + const { getLicenseStatus, notifications, location } = useAppContext(); const queryParams = new URLSearchParams(location.search); const indexName = queryParams.get('index'); const searchProfilerQueryURI = queryParams.get('load_from'); + const searchProfilerQuery = searchProfilerQueryURI && decompressFromEncodedURIComponent(searchProfilerQueryURI.replace(/^data:text\/plain,/, '')); + const [editorValue, setEditorValue] = useState( + searchProfilerQuery ? searchProfilerQuery : INITIAL_EDITOR_VALUE + ); const requestProfile = useRequestProfile(); const handleProfileClick = async () => { dispatch({ type: 'setProfiling', value: true }); try { - const { current: editor } = editorRef; const { data: result, error } = await requestProfile({ - query: editorRef.current.getValue(), + query: editorValue, index: indexInputRef.current.value, }); if (error) { notifications.addDanger(error); - editor.focus(); + editorPropsRef.current.focus(); return; } if (result === null) { @@ -78,18 +81,13 @@ export const ProfileQueryEditor = memo(() => { }; const onEditorReady = useCallback( - (editorInstance: any) => (editorRef.current = editorInstance), + (editorPropsInstance: EditorProps) => (editorPropsRef.current = editorPropsInstance), [] ); const licenseEnabled = getLicenseStatus().valid; return ( - + {/* Form */} @@ -120,9 +118,9 @@ export const ProfileQueryEditor = memo(() => { diff --git a/x-pack/plugins/searchprofiler/public/shared_imports.ts b/x-pack/plugins/searchprofiler/public/shared_imports.ts index b1af4bab9e62d..3daab65e28db8 100644 --- a/x-pack/plugins/searchprofiler/public/shared_imports.ts +++ b/x-pack/plugins/searchprofiler/public/shared_imports.ts @@ -5,6 +5,4 @@ * 2.0. */ -export { ace } from '@kbn/es-ui-shared-plugin/public'; - export { KibanaRenderContextProvider } from '@kbn/react-kibana-context-render'; diff --git a/x-pack/plugins/searchprofiler/tsconfig.json b/x-pack/plugins/searchprofiler/tsconfig.json index b99b0962e39fc..063b7dfa63ce6 100644 --- a/x-pack/plugins/searchprofiler/tsconfig.json +++ b/x-pack/plugins/searchprofiler/tsconfig.json @@ -20,9 +20,10 @@ "@kbn/expect", "@kbn/test-jest-helpers", "@kbn/i18n-react", - "@kbn/ace", "@kbn/config-schema", "@kbn/react-kibana-context-render", + "@kbn/code-editor", + "@kbn/monaco", ], "exclude": [ "target/**/*", diff --git a/x-pack/plugins/security/public/account_management/account_management_app.tsx b/x-pack/plugins/security/public/account_management/account_management_app.tsx index 6a812cf3ccb83..c224d5cbc40a1 100644 --- a/x-pack/plugins/security/public/account_management/account_management_app.tsx +++ b/x-pack/plugins/security/public/account_management/account_management_app.tsx @@ -6,7 +6,8 @@ */ import type { History } from 'history'; -import React, { FC, PropsWithChildren } from 'react'; +import type { FC, PropsWithChildren } from 'react'; +import React from 'react'; import { render, unmountComponentAtNode } from 'react-dom'; import type { diff --git a/x-pack/plugins/security/public/authentication/components/authentication_state_page/authentication_state_page.tsx b/x-pack/plugins/security/public/authentication/components/authentication_state_page/authentication_state_page.tsx index eea9763420884..e491192b79d52 100644 --- a/x-pack/plugins/security/public/authentication/components/authentication_state_page/authentication_state_page.tsx +++ b/x-pack/plugins/security/public/authentication/components/authentication_state_page/authentication_state_page.tsx @@ -8,7 +8,8 @@ import './authentication_state_page.scss'; import { EuiIcon, EuiImage, EuiSpacer, EuiTitle } from '@elastic/eui'; -import React, { FC, PropsWithChildren } from 'react'; +import type { FC, PropsWithChildren } from 'react'; +import React from 'react'; interface Props { className?: string; diff --git a/x-pack/plugins/security/public/management/role_mappings/edit_role_mapping/edit_role_mapping_page.test.tsx b/x-pack/plugins/security/public/management/role_mappings/edit_role_mapping/edit_role_mapping_page.test.tsx index e74d2f7703f31..63c395b1f4bbc 100644 --- a/x-pack/plugins/security/public/management/role_mappings/edit_role_mapping/edit_role_mapping_page.test.tsx +++ b/x-pack/plugins/security/public/management/role_mappings/edit_role_mapping/edit_role_mapping_page.test.tsx @@ -5,11 +5,6 @@ * 2.0. */ -// brace/ace uses the Worker class, which is not currently provided by JSDOM. -// This is not required for the tests to pass, but it rather suppresses lengthy -// warnings in the console which adds unnecessary noise to the test output. -import '@kbn/web-worker-stub'; - import React from 'react'; import { coreMock, scopedHistoryMock } from '@kbn/core/public/mocks'; diff --git a/x-pack/plugins/security/public/management/role_mappings/edit_role_mapping/rule_editor_panel/json_rule_editor.test.tsx b/x-pack/plugins/security/public/management/role_mappings/edit_role_mapping/rule_editor_panel/json_rule_editor.test.tsx index 64e332bd130bd..9db22a251779b 100644 --- a/x-pack/plugins/security/public/management/role_mappings/edit_role_mapping/rule_editor_panel/json_rule_editor.test.tsx +++ b/x-pack/plugins/security/public/management/role_mappings/edit_role_mapping/rule_editor_panel/json_rule_editor.test.tsx @@ -5,13 +5,6 @@ * 2.0. */ -import 'brace'; -import 'brace/mode/json'; -// brace/ace uses the Worker class, which is not currently provided by JSDOM. -// This is not required for the tests to pass, but it rather suppresses lengthy -// warnings in the console which adds unnecessary noise to the test output. -import '@kbn/web-worker-stub'; - import React from 'react'; import { act } from 'react-dom/test-utils'; import '@kbn/code-editor-mock/jest_helper'; diff --git a/x-pack/plugins/security/public/management/role_mappings/edit_role_mapping/rule_editor_panel/json_rule_editor.tsx b/x-pack/plugins/security/public/management/role_mappings/edit_role_mapping/rule_editor_panel/json_rule_editor.tsx index 963bbf3a35cfc..121f694517a83 100644 --- a/x-pack/plugins/security/public/management/role_mappings/edit_role_mapping/rule_editor_panel/json_rule_editor.tsx +++ b/x-pack/plugins/security/public/management/role_mappings/edit_role_mapping/rule_editor_panel/json_rule_editor.tsx @@ -5,10 +5,6 @@ * 2.0. */ -import 'react-ace'; -import 'brace/mode/json'; -import 'brace/theme/github'; - import { EuiButton, EuiFormRow, EuiLink, EuiSpacer, EuiText } from '@elastic/eui'; import React, { Fragment, useState } from 'react'; diff --git a/x-pack/plugins/security/public/management/role_mappings/edit_role_mapping/rule_editor_panel/rule_editor_panel.test.tsx b/x-pack/plugins/security/public/management/role_mappings/edit_role_mapping/rule_editor_panel/rule_editor_panel.test.tsx index d01229cdce8a9..21ece31571ae1 100644 --- a/x-pack/plugins/security/public/management/role_mappings/edit_role_mapping/rule_editor_panel/rule_editor_panel.test.tsx +++ b/x-pack/plugins/security/public/management/role_mappings/edit_role_mapping/rule_editor_panel/rule_editor_panel.test.tsx @@ -5,11 +5,6 @@ * 2.0. */ -// brace/ace uses the Worker class, which is not currently provided by JSDOM. -// This is not required for the tests to pass, but it rather suppresses lengthy -// warnings in the console which adds unnecessary noise to the test output. -import '@kbn/web-worker-stub'; - import { EuiErrorBoundary } from '@elastic/eui'; import React from 'react'; diff --git a/x-pack/plugins/security/public/management/users/users_management_app.tsx b/x-pack/plugins/security/public/management/users/users_management_app.tsx index aac45ae6c7934..eb005b68d812c 100644 --- a/x-pack/plugins/security/public/management/users/users_management_app.tsx +++ b/x-pack/plugins/security/public/management/users/users_management_app.tsx @@ -6,7 +6,8 @@ */ import type { History } from 'history'; -import React, { FC, PropsWithChildren } from 'react'; +import type { FC, PropsWithChildren } from 'react'; +import React from 'react'; import { render, unmountComponentAtNode } from 'react-dom'; import { Redirect } from 'react-router-dom'; diff --git a/x-pack/plugins/security/server/authorization/api_authorization.ts b/x-pack/plugins/security/server/authorization/api_authorization.ts index ba38d9ca0aa20..9c67ff8bdff8b 100644 --- a/x-pack/plugins/security/server/authorization/api_authorization.ts +++ b/x-pack/plugins/security/server/authorization/api_authorization.ts @@ -87,17 +87,17 @@ export function initAPIAuthorization( const missingPrivileges = Object.keys(kibanaPrivileges).filter( (key) => !kibanaPrivileges[key] ); - logger.warn( - `User not authorized for "${request.url.pathname}${ - request.url.search - }", responding with 403: missing privileges: ${missingPrivileges.join(', ')}` - ); + const forbiddenMessage = `API [${request.route.method.toUpperCase()} ${ + request.url.pathname + }${ + request.url.search + }] is unauthorized for user, this action is granted by the Kibana privileges [${missingPrivileges}]`; + + logger.warn(forbiddenMessage); return response.forbidden({ body: { - message: `User not authorized for ${request.url.pathname}${ - request.url.search - }, missing privileges: ${missingPrivileges.join(', ')}`, + message: forbiddenMessage, }, }); } diff --git a/x-pack/plugins/security/tsconfig.json b/x-pack/plugins/security/tsconfig.json index 535e221f8e5fb..2d5509d2d6d42 100644 --- a/x-pack/plugins/security/tsconfig.json +++ b/x-pack/plugins/security/tsconfig.json @@ -51,7 +51,6 @@ "@kbn/core-saved-objects-api-server-internal", "@kbn/core-saved-objects-api-server-mocks", "@kbn/logging-mocks", - "@kbn/web-worker-stub", "@kbn/core-saved-objects-utils-server", "@kbn/core-saved-objects-api-server", "@kbn/core-saved-objects-base-server-internal", diff --git a/x-pack/plugins/security_solution/common/api/detection_engine/model/rule_schema/rule_schemas.gen.ts b/x-pack/plugins/security_solution/common/api/detection_engine/model/rule_schema/rule_schemas.gen.ts index da4661ae8464c..e15ab0f06e082 100644 --- a/x-pack/plugins/security_solution/common/api/detection_engine/model/rule_schema/rule_schemas.gen.ts +++ b/x-pack/plugins/security_solution/common/api/detection_engine/model/rule_schema/rule_schemas.gen.ts @@ -68,13 +68,13 @@ import { SavedQueryId, KqlQueryLanguage, } from './common_attributes.gen'; +import { ResponseAction } from '../rule_response_actions/response_actions.gen'; import { RuleExecutionSummary } from '../../rule_monitoring/model/execution_summary.gen'; import { EventCategoryOverride, TiebreakerField, TimestampField, } from './specific_attributes/eql_attributes.gen'; -import { ResponseAction } from '../rule_response_actions/response_actions.gen'; import { Threshold, ThresholdAlertSuppression, @@ -117,6 +117,7 @@ export const BaseOptionalFields = z.object({ meta: RuleMetadata.optional(), investigation_fields: InvestigationFields.optional(), throttle: RuleActionThrottle.optional(), + response_actions: z.array(ResponseAction).optional(), }); export type BaseDefaultableFields = z.infer; @@ -224,7 +225,6 @@ export const EqlOptionalFields = z.object({ tiebreaker_field: TiebreakerField.optional(), timestamp_field: TimestampField.optional(), alert_suppression: AlertSuppression.optional(), - response_actions: z.array(ResponseAction).optional(), }); export type EqlRuleCreateFields = z.infer; @@ -262,7 +262,6 @@ export const QueryRuleOptionalFields = z.object({ data_view_id: DataViewId.optional(), filters: RuleFilterArray.optional(), saved_id: SavedQueryId.optional(), - response_actions: z.array(ResponseAction).optional(), alert_suppression: AlertSuppression.optional(), }); @@ -313,7 +312,6 @@ export const SavedQueryRuleOptionalFields = z.object({ index: IndexPatternArray.optional(), data_view_id: DataViewId.optional(), filters: RuleFilterArray.optional(), - response_actions: z.array(ResponseAction).optional(), alert_suppression: AlertSuppression.optional(), query: RuleQuery.optional(), }); @@ -522,7 +520,6 @@ export const NewTermsRuleOptionalFields = z.object({ data_view_id: DataViewId.optional(), filters: RuleFilterArray.optional(), alert_suppression: AlertSuppression.optional(), - response_actions: z.array(ResponseAction).optional(), }); export type NewTermsRuleDefaultableFields = z.infer; @@ -576,7 +573,6 @@ export const EsqlRuleRequiredFields = z.object({ export type EsqlRuleOptionalFields = z.infer; export const EsqlRuleOptionalFields = z.object({ alert_suppression: AlertSuppression.optional(), - response_actions: z.array(ResponseAction).optional(), }); export type EsqlRulePatchFields = z.infer; diff --git a/x-pack/plugins/security_solution/common/api/detection_engine/model/rule_schema/rule_schemas.schema.yaml b/x-pack/plugins/security_solution/common/api/detection_engine/model/rule_schema/rule_schemas.schema.yaml index d8aba232c26f9..f362b41ab6e86 100644 --- a/x-pack/plugins/security_solution/common/api/detection_engine/model/rule_schema/rule_schemas.schema.yaml +++ b/x-pack/plugins/security_solution/common/api/detection_engine/model/rule_schema/rule_schemas.schema.yaml @@ -74,6 +74,11 @@ components: throttle: $ref: './common_attributes.schema.yaml#/components/schemas/RuleActionThrottle' + response_actions: + type: array + items: + $ref: '../rule_response_actions/response_actions.schema.yaml#/components/schemas/ResponseAction' + BaseDefaultableFields: x-inline: true type: object @@ -293,10 +298,6 @@ components: $ref: './specific_attributes/eql_attributes.schema.yaml#/components/schemas/TimestampField' alert_suppression: $ref: './common_attributes.schema.yaml#/components/schemas/AlertSuppression' - response_actions: - type: array - items: - $ref: '../rule_response_actions/response_actions.schema.yaml#/components/schemas/ResponseAction' EqlRuleCreateFields: allOf: @@ -359,10 +360,6 @@ components: $ref: './common_attributes.schema.yaml#/components/schemas/RuleFilterArray' saved_id: $ref: './common_attributes.schema.yaml#/components/schemas/SavedQueryId' - response_actions: - type: array - items: - $ref: '../rule_response_actions/response_actions.schema.yaml#/components/schemas/ResponseAction' alert_suppression: $ref: './common_attributes.schema.yaml#/components/schemas/AlertSuppression' @@ -440,10 +437,6 @@ components: $ref: './common_attributes.schema.yaml#/components/schemas/DataViewId' filters: $ref: './common_attributes.schema.yaml#/components/schemas/RuleFilterArray' - response_actions: - type: array - items: - $ref: '../rule_response_actions/response_actions.schema.yaml#/components/schemas/ResponseAction' alert_suppression: $ref: './common_attributes.schema.yaml#/components/schemas/AlertSuppression' query: @@ -767,10 +760,6 @@ components: $ref: './common_attributes.schema.yaml#/components/schemas/RuleFilterArray' alert_suppression: $ref: './common_attributes.schema.yaml#/components/schemas/AlertSuppression' - response_actions: - type: array - items: - $ref: '../rule_response_actions/response_actions.schema.yaml#/components/schemas/ResponseAction' NewTermsRuleDefaultableFields: type: object @@ -849,10 +838,6 @@ components: properties: alert_suppression: $ref: './common_attributes.schema.yaml#/components/schemas/AlertSuppression' - response_actions: - type: array - items: - $ref: '../rule_response_actions/response_actions.schema.yaml#/components/schemas/ResponseAction' EsqlRulePatchFields: allOf: diff --git a/x-pack/plugins/security_solution/common/api/endpoint/metadata/get_metadata.schema.yaml b/x-pack/plugins/security_solution/common/api/endpoint/metadata/get_metadata.schema.yaml index 680eb1b3be7ed..9bbcd11716513 100644 --- a/x-pack/plugins/security_solution/common/api/endpoint/metadata/get_metadata.schema.yaml +++ b/x-pack/plugins/security_solution/common/api/endpoint/metadata/get_metadata.schema.yaml @@ -25,6 +25,7 @@ paths: /api/endpoint/metadata/transforms: get: + deprecated: true summary: Get metadata transforms operationId: GetEndpointMetadataTransform x-codegen-enabled: false diff --git a/x-pack/plugins/security_solution/common/detection_engine/constants.ts b/x-pack/plugins/security_solution/common/detection_engine/constants.ts index 7057e3c8b3091..270af1a91cf46 100644 --- a/x-pack/plugins/security_solution/common/detection_engine/constants.ts +++ b/x-pack/plugins/security_solution/common/detection_engine/constants.ts @@ -51,4 +51,12 @@ export const SUPPRESSIBLE_ALERT_RULES: Type[] = [ 'machine_learning', ]; -export const SUPPRESSIBLE_ALERT_RULES_GA: Type[] = ['saved_query', 'query']; +export const SUPPRESSIBLE_ALERT_RULES_GA: Type[] = [ + 'threshold', + 'esql', + 'saved_query', + 'query', + 'new_terms', + 'threat_match', + 'machine_learning', +]; diff --git a/x-pack/plugins/security_solution/common/detection_engine/utils.test.ts b/x-pack/plugins/security_solution/common/detection_engine/utils.test.ts index a4db006a67463..be0b6ce9c2927 100644 --- a/x-pack/plugins/security_solution/common/detection_engine/utils.test.ts +++ b/x-pack/plugins/security_solution/common/detection_engine/utils.test.ts @@ -250,14 +250,14 @@ describe('Alert Suppression Rules', () => { test('should return true for rule type suppression in global availability', () => { expect(isSuppressionRuleInGA('saved_query')).toBe(true); expect(isSuppressionRuleInGA('query')).toBe(true); + expect(isSuppressionRuleInGA('esql')).toBe(true); + expect(isSuppressionRuleInGA('threshold')).toBe(true); + expect(isSuppressionRuleInGA('threat_match')).toBe(true); + expect(isSuppressionRuleInGA('new_terms')).toBe(true); + expect(isSuppressionRuleInGA('machine_learning')).toBe(true); }); test('should return false for rule type suppression in tech preview', () => { - expect(isSuppressionRuleInGA('machine_learning')).toBe(false); - expect(isSuppressionRuleInGA('esql')).toBe(false); - expect(isSuppressionRuleInGA('threshold')).toBe(false); - expect(isSuppressionRuleInGA('threat_match')).toBe(false); - expect(isSuppressionRuleInGA('new_terms')).toBe(false); expect(isSuppressionRuleInGA('eql')).toBe(false); }); }); diff --git a/x-pack/plugins/security_solution/common/detection_engine/utils.ts b/x-pack/plugins/security_solution/common/detection_engine/utils.ts index 5068f35b6be1a..a98ca169a41d7 100644 --- a/x-pack/plugins/security_solution/common/detection_engine/utils.ts +++ b/x-pack/plugins/security_solution/common/detection_engine/utils.ts @@ -93,9 +93,16 @@ export const isSuppressionRuleConfiguredWithMissingFields = (ruleType: Type) => export const isSuppressionRuleInGA = (ruleType: Type): boolean => { return isSuppressibleAlertRule(ruleType) && SUPPRESSIBLE_ALERT_RULES_GA.includes(ruleType); }; - -export const shouldShowResponseActions = (ruleType: Type | undefined) => { +export const shouldShowResponseActions = ( + ruleType: Type | undefined, + automatedResponseActionsForAllRulesEnabled: boolean +) => { return ( - isQueryRule(ruleType) || isEsqlRule(ruleType) || isEqlRule(ruleType) || isNewTermsRule(ruleType) + isQueryRule(ruleType) || + isEsqlRule(ruleType) || + isEqlRule(ruleType) || + isNewTermsRule(ruleType) || + (automatedResponseActionsForAllRulesEnabled && + (isThresholdRule(ruleType) || isThreatMatchRule(ruleType) || isMlRule(ruleType))) ); }; diff --git a/x-pack/plugins/security_solution/common/endpoint/constants.ts b/x-pack/plugins/security_solution/common/endpoint/constants.ts index 0e7218f0d7d41..2a11d047dd865 100644 --- a/x-pack/plugins/security_solution/common/endpoint/constants.ts +++ b/x-pack/plugins/security_solution/common/endpoint/constants.ts @@ -65,8 +65,12 @@ export const BASE_INTERNAL_ENDPOINT_ROUTE = `/internal${BASE_ENDPOINT_ROUTE}`; // Endpoint API routes export const HOST_METADATA_LIST_ROUTE = `${BASE_ENDPOINT_ROUTE}/metadata`; export const HOST_METADATA_GET_ROUTE = `${HOST_METADATA_LIST_ROUTE}/{id}`; + +/** @deprecated public route, use {@link METADATA_TRANSFORMS_STATUS_INTERNAL_ROUTE} internal route */ export const METADATA_TRANSFORMS_STATUS_ROUTE = `${BASE_ENDPOINT_ROUTE}/metadata/transforms`; +export const METADATA_TRANSFORMS_STATUS_INTERNAL_ROUTE = `${BASE_INTERNAL_ENDPOINT_ROUTE}/metadata/transforms`; + export const BASE_POLICY_RESPONSE_ROUTE = `${BASE_ENDPOINT_ROUTE}/policy_response`; export const BASE_POLICY_ROUTE = `${BASE_ENDPOINT_ROUTE}/policy`; export const AGENT_POLICY_SUMMARY_ROUTE = `${BASE_POLICY_ROUTE}/summaries`; diff --git a/x-pack/plugins/security_solution/common/experimental_features.ts b/x-pack/plugins/security_solution/common/experimental_features.ts index 8ee859421d012..1e5ffee50afc7 100644 --- a/x-pack/plugins/security_solution/common/experimental_features.ts +++ b/x-pack/plugins/security_solution/common/experimental_features.ts @@ -52,6 +52,11 @@ export const allowedExperimentalValues = Object.freeze({ */ automatedProcessActionsEnabled: true, + /** + * Temporary feature flag to enable the Response Actions in Rules UI - intermediate release + */ + automatedResponseActionsForAllRulesEnabled: false, + /** * Enables the ability to send Response actions to SentinelOne and persist the results * in ES. Adds API changes to support `agentType` and supports `isolate` and `release` @@ -113,11 +118,6 @@ export const allowedExperimentalValues = Object.freeze({ */ assistantKnowledgeBaseByDefault: false, - /** - * Enables the NaturalLanguageESQLTool and disables the ESQLKnowledgeBaseTool, introduced in `8.16.0`. - */ - assistantNaturalLanguageESQLTool: false, - /** * Enables the Managed User section inside the new user details flyout. */ @@ -138,11 +138,6 @@ export const allowedExperimentalValues = Object.freeze({ */ esqlRulesDisabled: false, - /** - * enables logging requests during rule preview - */ - loggingRequestsEnabled: false, - /** * Enables Protection Updates tab in the Endpoint Policy Details page */ @@ -230,11 +225,6 @@ export const allowedExperimentalValues = Object.freeze({ */ valueListItemsModalEnabled: true, - /** - * Enables the manual rule run - */ - manualRuleRunEnabled: false, - /** * Adds a new option to filter descendants of a process for Management / Event Filters */ diff --git a/x-pack/plugins/security_solution/docs/openapi/ess/security_solution_detections_api_2023_10_31.bundled.schema.yaml b/x-pack/plugins/security_solution/docs/openapi/ess/security_solution_detections_api_2023_10_31.bundled.schema.yaml index 8fca765f4fb3f..ebd4c93280090 100644 --- a/x-pack/plugins/security_solution/docs/openapi/ess/security_solution_detections_api_2023_10_31.bundled.schema.yaml +++ b/x-pack/plugins/security_solution/docs/openapi/ess/security_solution_detections_api_2023_10_31.bundled.schema.yaml @@ -2051,10 +2051,6 @@ components: $ref: '#/components/schemas/RuleFilterArray' index: $ref: '#/components/schemas/IndexPatternArray' - response_actions: - items: - $ref: '#/components/schemas/ResponseAction' - type: array tiebreaker_field: $ref: '#/components/schemas/TiebreakerField' timestamp_field: @@ -2137,6 +2133,10 @@ components: items: $ref: '#/components/schemas/RequiredFieldInput' type: array + response_actions: + items: + $ref: '#/components/schemas/ResponseAction' + type: array risk_score: $ref: '#/components/schemas/RiskScore' risk_score_mapping: @@ -2252,6 +2252,10 @@ components: items: $ref: '#/components/schemas/RequiredFieldInput' type: array + response_actions: + items: + $ref: '#/components/schemas/ResponseAction' + type: array risk_score: $ref: '#/components/schemas/RiskScore' risk_score_mapping: @@ -2364,6 +2368,10 @@ components: items: $ref: '#/components/schemas/RequiredFieldInput' type: array + response_actions: + items: + $ref: '#/components/schemas/ResponseAction' + type: array risk_score: $ref: '#/components/schemas/RiskScore' risk_score_mapping: @@ -2459,6 +2467,10 @@ components: items: $ref: '#/components/schemas/RequiredFieldInput' type: array + response_actions: + items: + $ref: '#/components/schemas/ResponseAction' + type: array risk_score: $ref: '#/components/schemas/RiskScore' risk_score_mapping: @@ -2584,6 +2596,10 @@ components: items: $ref: '#/components/schemas/RequiredFieldInput' type: array + response_actions: + items: + $ref: '#/components/schemas/ResponseAction' + type: array risk_score: $ref: '#/components/schemas/RiskScore' risk_score_mapping: @@ -2699,6 +2715,10 @@ components: items: $ref: '#/components/schemas/RequiredFieldInput' type: array + response_actions: + items: + $ref: '#/components/schemas/ResponseAction' + type: array risk_score: $ref: '#/components/schemas/RiskScore' risk_score_mapping: @@ -2742,10 +2762,6 @@ components: properties: alert_suppression: $ref: '#/components/schemas/AlertSuppression' - response_actions: - items: - $ref: '#/components/schemas/ResponseAction' - type: array EsqlRulePatchProps: allOf: - type: object @@ -2809,6 +2825,10 @@ components: items: $ref: '#/components/schemas/RequiredFieldInput' type: array + response_actions: + items: + $ref: '#/components/schemas/ResponseAction' + type: array risk_score: $ref: '#/components/schemas/RiskScore' risk_score_mapping: @@ -2926,6 +2946,10 @@ components: items: $ref: '#/components/schemas/RequiredFieldInput' type: array + response_actions: + items: + $ref: '#/components/schemas/ResponseAction' + type: array risk_score: $ref: '#/components/schemas/RiskScore' risk_score_mapping: @@ -3178,6 +3202,10 @@ components: items: $ref: '#/components/schemas/RequiredFieldInput' type: array + response_actions: + items: + $ref: '#/components/schemas/ResponseAction' + type: array risk_score: $ref: '#/components/schemas/RiskScore' risk_score_mapping: @@ -3293,6 +3321,10 @@ components: items: $ref: '#/components/schemas/RequiredFieldInput' type: array + response_actions: + items: + $ref: '#/components/schemas/ResponseAction' + type: array risk_score: $ref: '#/components/schemas/RiskScore' risk_score_mapping: @@ -3408,6 +3440,10 @@ components: items: $ref: '#/components/schemas/RequiredFieldInput' type: array + response_actions: + items: + $ref: '#/components/schemas/ResponseAction' + type: array risk_score: $ref: '#/components/schemas/RiskScore' risk_score_mapping: @@ -3519,6 +3555,10 @@ components: items: $ref: '#/components/schemas/RequiredFieldInput' type: array + response_actions: + items: + $ref: '#/components/schemas/ResponseAction' + type: array risk_score: $ref: '#/components/schemas/RiskScore' risk_score_mapping: @@ -3720,6 +3760,10 @@ components: items: $ref: '#/components/schemas/RequiredFieldInput' type: array + response_actions: + items: + $ref: '#/components/schemas/ResponseAction' + type: array risk_score: $ref: '#/components/schemas/RiskScore' risk_score_mapping: @@ -3836,6 +3880,10 @@ components: items: $ref: '#/components/schemas/RequiredFieldInput' type: array + response_actions: + items: + $ref: '#/components/schemas/ResponseAction' + type: array risk_score: $ref: '#/components/schemas/RiskScore' risk_score_mapping: @@ -3890,10 +3938,6 @@ components: $ref: '#/components/schemas/RuleFilterArray' index: $ref: '#/components/schemas/IndexPatternArray' - response_actions: - items: - $ref: '#/components/schemas/ResponseAction' - type: array NewTermsRulePatchFields: allOf: - type: object @@ -3969,6 +4013,10 @@ components: items: $ref: '#/components/schemas/RequiredFieldInput' type: array + response_actions: + items: + $ref: '#/components/schemas/ResponseAction' + type: array risk_score: $ref: '#/components/schemas/RiskScore' risk_score_mapping: @@ -4089,6 +4137,10 @@ components: items: $ref: '#/components/schemas/RequiredFieldInput' type: array + response_actions: + items: + $ref: '#/components/schemas/ResponseAction' + type: array risk_score: $ref: '#/components/schemas/RiskScore' risk_score_mapping: @@ -4312,6 +4364,10 @@ components: items: $ref: '#/components/schemas/RequiredFieldInput' type: array + response_actions: + items: + $ref: '#/components/schemas/ResponseAction' + type: array risk_score: $ref: '#/components/schemas/RiskScore' risk_score_mapping: @@ -4428,6 +4484,10 @@ components: items: $ref: '#/components/schemas/RequiredFieldInput' type: array + response_actions: + items: + $ref: '#/components/schemas/ResponseAction' + type: array risk_score: $ref: '#/components/schemas/RiskScore' risk_score_mapping: @@ -4484,10 +4544,6 @@ components: $ref: '#/components/schemas/RuleFilterArray' index: $ref: '#/components/schemas/IndexPatternArray' - response_actions: - items: - $ref: '#/components/schemas/ResponseAction' - type: array saved_id: $ref: '#/components/schemas/SavedQueryId' QueryRulePatchFields: @@ -4559,6 +4615,10 @@ components: items: $ref: '#/components/schemas/RequiredFieldInput' type: array + response_actions: + items: + $ref: '#/components/schemas/ResponseAction' + type: array risk_score: $ref: '#/components/schemas/RiskScore' risk_score_mapping: @@ -4673,6 +4733,10 @@ components: items: $ref: '#/components/schemas/RequiredFieldInput' type: array + response_actions: + items: + $ref: '#/components/schemas/ResponseAction' + type: array risk_score: $ref: '#/components/schemas/RiskScore' risk_score_mapping: @@ -5359,6 +5423,10 @@ components: items: $ref: '#/components/schemas/RequiredFieldInput' type: array + response_actions: + items: + $ref: '#/components/schemas/ResponseAction' + type: array risk_score: $ref: '#/components/schemas/RiskScore' risk_score_mapping: @@ -5475,6 +5543,10 @@ components: items: $ref: '#/components/schemas/RequiredFieldInput' type: array + response_actions: + items: + $ref: '#/components/schemas/ResponseAction' + type: array risk_score: $ref: '#/components/schemas/RiskScore' risk_score_mapping: @@ -5531,10 +5603,6 @@ components: $ref: '#/components/schemas/IndexPatternArray' query: $ref: '#/components/schemas/RuleQuery' - response_actions: - items: - $ref: '#/components/schemas/ResponseAction' - type: array SavedQueryRulePatchFields: allOf: - type: object @@ -5606,6 +5674,10 @@ components: items: $ref: '#/components/schemas/RequiredFieldInput' type: array + response_actions: + items: + $ref: '#/components/schemas/ResponseAction' + type: array risk_score: $ref: '#/components/schemas/RiskScore' risk_score_mapping: @@ -5720,6 +5792,10 @@ components: items: $ref: '#/components/schemas/RequiredFieldInput' type: array + response_actions: + items: + $ref: '#/components/schemas/ResponseAction' + type: array risk_score: $ref: '#/components/schemas/RiskScore' risk_score_mapping: @@ -5967,6 +6043,10 @@ components: items: $ref: '#/components/schemas/RequiredFieldInput' type: array + response_actions: + items: + $ref: '#/components/schemas/ResponseAction' + type: array risk_score: $ref: '#/components/schemas/RiskScore' risk_score_mapping: @@ -6083,6 +6163,10 @@ components: items: $ref: '#/components/schemas/RequiredFieldInput' type: array + response_actions: + items: + $ref: '#/components/schemas/ResponseAction' + type: array risk_score: $ref: '#/components/schemas/RiskScore' risk_score_mapping: @@ -6226,6 +6310,10 @@ components: items: $ref: '#/components/schemas/RequiredFieldInput' type: array + response_actions: + items: + $ref: '#/components/schemas/ResponseAction' + type: array risk_score: $ref: '#/components/schemas/RiskScore' risk_score_mapping: @@ -6349,6 +6437,10 @@ components: items: $ref: '#/components/schemas/RequiredFieldInput' type: array + response_actions: + items: + $ref: '#/components/schemas/ResponseAction' + type: array risk_score: $ref: '#/components/schemas/RiskScore' risk_score_mapping: @@ -6538,6 +6630,10 @@ components: items: $ref: '#/components/schemas/RequiredFieldInput' type: array + response_actions: + items: + $ref: '#/components/schemas/ResponseAction' + type: array risk_score: $ref: '#/components/schemas/RiskScore' risk_score_mapping: @@ -6654,6 +6750,10 @@ components: items: $ref: '#/components/schemas/RequiredFieldInput' type: array + response_actions: + items: + $ref: '#/components/schemas/ResponseAction' + type: array risk_score: $ref: '#/components/schemas/RiskScore' risk_score_mapping: @@ -6783,6 +6883,10 @@ components: items: $ref: '#/components/schemas/RequiredFieldInput' type: array + response_actions: + items: + $ref: '#/components/schemas/ResponseAction' + type: array risk_score: $ref: '#/components/schemas/RiskScore' risk_score_mapping: @@ -6900,6 +7004,10 @@ components: items: $ref: '#/components/schemas/RequiredFieldInput' type: array + response_actions: + items: + $ref: '#/components/schemas/ResponseAction' + type: array risk_score: $ref: '#/components/schemas/RiskScore' risk_score_mapping: diff --git a/x-pack/plugins/security_solution/docs/openapi/ess/security_solution_endpoint_management_api_2023_10_31.bundled.schema.yaml b/x-pack/plugins/security_solution/docs/openapi/ess/security_solution_endpoint_management_api_2023_10_31.bundled.schema.yaml index 5d03e10969e14..60ac76e240fa4 100644 --- a/x-pack/plugins/security_solution/docs/openapi/ess/security_solution_endpoint_management_api_2023_10_31.bundled.schema.yaml +++ b/x-pack/plugins/security_solution/docs/openapi/ess/security_solution_endpoint_management_api_2023_10_31.bundled.schema.yaml @@ -438,6 +438,7 @@ paths: - Security Endpoint Management API /api/endpoint/metadata/transforms: get: + deprecated: true operationId: GetEndpointMetadataTransform responses: '200': diff --git a/x-pack/plugins/security_solution/docs/openapi/serverless/security_solution_detections_api_2023_10_31.bundled.schema.yaml b/x-pack/plugins/security_solution/docs/openapi/serverless/security_solution_detections_api_2023_10_31.bundled.schema.yaml index 38b419972a681..30a7ae3ea343e 100644 --- a/x-pack/plugins/security_solution/docs/openapi/serverless/security_solution_detections_api_2023_10_31.bundled.schema.yaml +++ b/x-pack/plugins/security_solution/docs/openapi/serverless/security_solution_detections_api_2023_10_31.bundled.schema.yaml @@ -1325,10 +1325,6 @@ components: $ref: '#/components/schemas/RuleFilterArray' index: $ref: '#/components/schemas/IndexPatternArray' - response_actions: - items: - $ref: '#/components/schemas/ResponseAction' - type: array tiebreaker_field: $ref: '#/components/schemas/TiebreakerField' timestamp_field: @@ -1411,6 +1407,10 @@ components: items: $ref: '#/components/schemas/RequiredFieldInput' type: array + response_actions: + items: + $ref: '#/components/schemas/ResponseAction' + type: array risk_score: $ref: '#/components/schemas/RiskScore' risk_score_mapping: @@ -1526,6 +1526,10 @@ components: items: $ref: '#/components/schemas/RequiredFieldInput' type: array + response_actions: + items: + $ref: '#/components/schemas/ResponseAction' + type: array risk_score: $ref: '#/components/schemas/RiskScore' risk_score_mapping: @@ -1638,6 +1642,10 @@ components: items: $ref: '#/components/schemas/RequiredFieldInput' type: array + response_actions: + items: + $ref: '#/components/schemas/ResponseAction' + type: array risk_score: $ref: '#/components/schemas/RiskScore' risk_score_mapping: @@ -1733,6 +1741,10 @@ components: items: $ref: '#/components/schemas/RequiredFieldInput' type: array + response_actions: + items: + $ref: '#/components/schemas/ResponseAction' + type: array risk_score: $ref: '#/components/schemas/RiskScore' risk_score_mapping: @@ -1858,6 +1870,10 @@ components: items: $ref: '#/components/schemas/RequiredFieldInput' type: array + response_actions: + items: + $ref: '#/components/schemas/ResponseAction' + type: array risk_score: $ref: '#/components/schemas/RiskScore' risk_score_mapping: @@ -1973,6 +1989,10 @@ components: items: $ref: '#/components/schemas/RequiredFieldInput' type: array + response_actions: + items: + $ref: '#/components/schemas/ResponseAction' + type: array risk_score: $ref: '#/components/schemas/RiskScore' risk_score_mapping: @@ -2016,10 +2036,6 @@ components: properties: alert_suppression: $ref: '#/components/schemas/AlertSuppression' - response_actions: - items: - $ref: '#/components/schemas/ResponseAction' - type: array EsqlRulePatchProps: allOf: - type: object @@ -2083,6 +2099,10 @@ components: items: $ref: '#/components/schemas/RequiredFieldInput' type: array + response_actions: + items: + $ref: '#/components/schemas/ResponseAction' + type: array risk_score: $ref: '#/components/schemas/RiskScore' risk_score_mapping: @@ -2200,6 +2220,10 @@ components: items: $ref: '#/components/schemas/RequiredFieldInput' type: array + response_actions: + items: + $ref: '#/components/schemas/ResponseAction' + type: array risk_score: $ref: '#/components/schemas/RiskScore' risk_score_mapping: @@ -2429,6 +2453,10 @@ components: items: $ref: '#/components/schemas/RequiredFieldInput' type: array + response_actions: + items: + $ref: '#/components/schemas/ResponseAction' + type: array risk_score: $ref: '#/components/schemas/RiskScore' risk_score_mapping: @@ -2544,6 +2572,10 @@ components: items: $ref: '#/components/schemas/RequiredFieldInput' type: array + response_actions: + items: + $ref: '#/components/schemas/ResponseAction' + type: array risk_score: $ref: '#/components/schemas/RiskScore' risk_score_mapping: @@ -2659,6 +2691,10 @@ components: items: $ref: '#/components/schemas/RequiredFieldInput' type: array + response_actions: + items: + $ref: '#/components/schemas/ResponseAction' + type: array risk_score: $ref: '#/components/schemas/RiskScore' risk_score_mapping: @@ -2770,6 +2806,10 @@ components: items: $ref: '#/components/schemas/RequiredFieldInput' type: array + response_actions: + items: + $ref: '#/components/schemas/ResponseAction' + type: array risk_score: $ref: '#/components/schemas/RiskScore' risk_score_mapping: @@ -2873,6 +2913,10 @@ components: items: $ref: '#/components/schemas/RequiredFieldInput' type: array + response_actions: + items: + $ref: '#/components/schemas/ResponseAction' + type: array risk_score: $ref: '#/components/schemas/RiskScore' risk_score_mapping: @@ -2989,6 +3033,10 @@ components: items: $ref: '#/components/schemas/RequiredFieldInput' type: array + response_actions: + items: + $ref: '#/components/schemas/ResponseAction' + type: array risk_score: $ref: '#/components/schemas/RiskScore' risk_score_mapping: @@ -3043,10 +3091,6 @@ components: $ref: '#/components/schemas/RuleFilterArray' index: $ref: '#/components/schemas/IndexPatternArray' - response_actions: - items: - $ref: '#/components/schemas/ResponseAction' - type: array NewTermsRulePatchFields: allOf: - type: object @@ -3122,6 +3166,10 @@ components: items: $ref: '#/components/schemas/RequiredFieldInput' type: array + response_actions: + items: + $ref: '#/components/schemas/ResponseAction' + type: array risk_score: $ref: '#/components/schemas/RiskScore' risk_score_mapping: @@ -3242,6 +3290,10 @@ components: items: $ref: '#/components/schemas/RequiredFieldInput' type: array + response_actions: + items: + $ref: '#/components/schemas/ResponseAction' + type: array risk_score: $ref: '#/components/schemas/RiskScore' risk_score_mapping: @@ -3465,6 +3517,10 @@ components: items: $ref: '#/components/schemas/RequiredFieldInput' type: array + response_actions: + items: + $ref: '#/components/schemas/ResponseAction' + type: array risk_score: $ref: '#/components/schemas/RiskScore' risk_score_mapping: @@ -3581,6 +3637,10 @@ components: items: $ref: '#/components/schemas/RequiredFieldInput' type: array + response_actions: + items: + $ref: '#/components/schemas/ResponseAction' + type: array risk_score: $ref: '#/components/schemas/RiskScore' risk_score_mapping: @@ -3637,10 +3697,6 @@ components: $ref: '#/components/schemas/RuleFilterArray' index: $ref: '#/components/schemas/IndexPatternArray' - response_actions: - items: - $ref: '#/components/schemas/ResponseAction' - type: array saved_id: $ref: '#/components/schemas/SavedQueryId' QueryRulePatchFields: @@ -3712,6 +3768,10 @@ components: items: $ref: '#/components/schemas/RequiredFieldInput' type: array + response_actions: + items: + $ref: '#/components/schemas/ResponseAction' + type: array risk_score: $ref: '#/components/schemas/RiskScore' risk_score_mapping: @@ -3826,6 +3886,10 @@ components: items: $ref: '#/components/schemas/RequiredFieldInput' type: array + response_actions: + items: + $ref: '#/components/schemas/ResponseAction' + type: array risk_score: $ref: '#/components/schemas/RiskScore' risk_score_mapping: @@ -4512,6 +4576,10 @@ components: items: $ref: '#/components/schemas/RequiredFieldInput' type: array + response_actions: + items: + $ref: '#/components/schemas/ResponseAction' + type: array risk_score: $ref: '#/components/schemas/RiskScore' risk_score_mapping: @@ -4628,6 +4696,10 @@ components: items: $ref: '#/components/schemas/RequiredFieldInput' type: array + response_actions: + items: + $ref: '#/components/schemas/ResponseAction' + type: array risk_score: $ref: '#/components/schemas/RiskScore' risk_score_mapping: @@ -4684,10 +4756,6 @@ components: $ref: '#/components/schemas/IndexPatternArray' query: $ref: '#/components/schemas/RuleQuery' - response_actions: - items: - $ref: '#/components/schemas/ResponseAction' - type: array SavedQueryRulePatchFields: allOf: - type: object @@ -4759,6 +4827,10 @@ components: items: $ref: '#/components/schemas/RequiredFieldInput' type: array + response_actions: + items: + $ref: '#/components/schemas/ResponseAction' + type: array risk_score: $ref: '#/components/schemas/RiskScore' risk_score_mapping: @@ -4873,6 +4945,10 @@ components: items: $ref: '#/components/schemas/RequiredFieldInput' type: array + response_actions: + items: + $ref: '#/components/schemas/ResponseAction' + type: array risk_score: $ref: '#/components/schemas/RiskScore' risk_score_mapping: @@ -5113,6 +5189,10 @@ components: items: $ref: '#/components/schemas/RequiredFieldInput' type: array + response_actions: + items: + $ref: '#/components/schemas/ResponseAction' + type: array risk_score: $ref: '#/components/schemas/RiskScore' risk_score_mapping: @@ -5229,6 +5309,10 @@ components: items: $ref: '#/components/schemas/RequiredFieldInput' type: array + response_actions: + items: + $ref: '#/components/schemas/ResponseAction' + type: array risk_score: $ref: '#/components/schemas/RiskScore' risk_score_mapping: @@ -5372,6 +5456,10 @@ components: items: $ref: '#/components/schemas/RequiredFieldInput' type: array + response_actions: + items: + $ref: '#/components/schemas/ResponseAction' + type: array risk_score: $ref: '#/components/schemas/RiskScore' risk_score_mapping: @@ -5495,6 +5583,10 @@ components: items: $ref: '#/components/schemas/RequiredFieldInput' type: array + response_actions: + items: + $ref: '#/components/schemas/ResponseAction' + type: array risk_score: $ref: '#/components/schemas/RiskScore' risk_score_mapping: @@ -5684,6 +5776,10 @@ components: items: $ref: '#/components/schemas/RequiredFieldInput' type: array + response_actions: + items: + $ref: '#/components/schemas/ResponseAction' + type: array risk_score: $ref: '#/components/schemas/RiskScore' risk_score_mapping: @@ -5800,6 +5896,10 @@ components: items: $ref: '#/components/schemas/RequiredFieldInput' type: array + response_actions: + items: + $ref: '#/components/schemas/ResponseAction' + type: array risk_score: $ref: '#/components/schemas/RiskScore' risk_score_mapping: @@ -5929,6 +6029,10 @@ components: items: $ref: '#/components/schemas/RequiredFieldInput' type: array + response_actions: + items: + $ref: '#/components/schemas/ResponseAction' + type: array risk_score: $ref: '#/components/schemas/RiskScore' risk_score_mapping: @@ -6046,6 +6150,10 @@ components: items: $ref: '#/components/schemas/RequiredFieldInput' type: array + response_actions: + items: + $ref: '#/components/schemas/ResponseAction' + type: array risk_score: $ref: '#/components/schemas/RiskScore' risk_score_mapping: diff --git a/x-pack/plugins/security_solution/docs/openapi/serverless/security_solution_endpoint_management_api_2023_10_31.bundled.schema.yaml b/x-pack/plugins/security_solution/docs/openapi/serverless/security_solution_endpoint_management_api_2023_10_31.bundled.schema.yaml index f5c2d290af33c..7bd94b9c8227c 100644 --- a/x-pack/plugins/security_solution/docs/openapi/serverless/security_solution_endpoint_management_api_2023_10_31.bundled.schema.yaml +++ b/x-pack/plugins/security_solution/docs/openapi/serverless/security_solution_endpoint_management_api_2023_10_31.bundled.schema.yaml @@ -388,6 +388,7 @@ paths: - Security Endpoint Management API /api/endpoint/metadata/transforms: get: + deprecated: true operationId: GetEndpointMetadataTransform responses: '200': diff --git a/x-pack/plugins/security_solution/public/assistant/stack_management/management_settings.test.tsx b/x-pack/plugins/security_solution/public/assistant/stack_management/management_settings.test.tsx index 1c988d14e845f..65a0ab84d3412 100644 --- a/x-pack/plugins/security_solution/public/assistant/stack_management/management_settings.test.tsx +++ b/x-pack/plugins/security_solution/public/assistant/stack_management/management_settings.test.tsx @@ -77,6 +77,11 @@ describe('ManagementSettings', () => { securitySolutionAssistant: { 'ai-assistant': false }, }, }, + data: { + dataViews: { + getIndices: jest.fn(), + }, + }, security: { userProfiles: { getCurrent: jest.fn().mockResolvedValue({ data: { color: 'blue', initials: 'P' } }), diff --git a/x-pack/plugins/security_solution/public/assistant/stack_management/management_settings.tsx b/x-pack/plugins/security_solution/public/assistant/stack_management/management_settings.tsx index 90e39398474ec..48d89e02dfc71 100644 --- a/x-pack/plugins/security_solution/public/assistant/stack_management/management_settings.tsx +++ b/x-pack/plugins/security_solution/public/assistant/stack_management/management_settings.tsx @@ -37,6 +37,7 @@ export const ManagementSettings = React.memo(() => { securitySolutionAssistant: { 'ai-assistant': securityAIAssistantEnabled }, }, }, + data: { dataViews }, security, } = useKibana().services; @@ -46,8 +47,8 @@ export const ManagementSettings = React.memo(() => { security?.userProfiles.getCurrent<{ avatar: UserAvatar }>({ dataPath: 'avatar', }), - select: (data) => { - return data.data.avatar; + select: (d) => { + return d.data.avatar; }, keepPreviousData: true, refetchOnWindowFocus: false, @@ -79,7 +80,12 @@ export const ManagementSettings = React.memo(() => { } if (conversations) { - return ; + return ( + + ); } return <>; diff --git a/x-pack/plugins/security_solution/public/attack_discovery/use_attack_discovery/helpers.ts b/x-pack/plugins/security_solution/public/attack_discovery/use_attack_discovery/helpers.ts index f800651985217..97eb132bdaaeb 100644 --- a/x-pack/plugins/security_solution/public/attack_discovery/use_attack_discovery/helpers.ts +++ b/x-pack/plugins/security_solution/public/attack_discovery/use_attack_discovery/helpers.ts @@ -18,6 +18,7 @@ import { isEmpty } from 'lodash/fp'; enum OpenAiProviderType { OpenAi = 'OpenAI', AzureAi = 'Azure OpenAI', + Other = 'Other', } interface GenAiConfig { diff --git a/x-pack/plugins/security_solution/public/cloud_security_posture/components/csp_details/insights_tab_csp.tsx b/x-pack/plugins/security_solution/public/cloud_security_posture/components/csp_details/insights_tab_csp.tsx index 595aaf5127ca3..05421cfa7a208 100644 --- a/x-pack/plugins/security_solution/public/cloud_security_posture/components/csp_details/insights_tab_csp.tsx +++ b/x-pack/plugins/security_solution/public/cloud_security_posture/components/csp_details/insights_tab_csp.tsx @@ -5,19 +5,134 @@ * 2.0. */ -import React, { memo } from 'react'; -import { EuiSpacer } from '@elastic/eui'; +import React, { memo, useMemo, useState } from 'react'; +import type { EuiButtonGroupOptionProps } from '@elastic/eui'; +import { EuiButtonGroup, EuiSpacer } from '@elastic/eui'; +import { FormattedMessage } from '@kbn/i18n-react'; +import type { FlyoutPanelProps, PanelPath } from '@kbn/expandable-flyout'; +import { useExpandableFlyoutState } from '@kbn/expandable-flyout'; +import { i18n } from '@kbn/i18n'; +// import type { FlyoutPanels } from '@kbn/expandable-flyout/src/store/state'; +import { CspInsightLeftPanelSubTab } from '../../../flyout/entity_details/shared/components/left_panel/left_panel_header'; import { MisconfigurationFindingsDetailsTable } from './misconfiguration_findings_details_table'; +import { VulnerabilitiesFindingsDetailsTable } from './vulnerabilities_findings_details_table'; /** * Insights view displayed in the document details expandable flyout left section */ + +interface CspFlyoutPanelProps extends FlyoutPanelProps { + params: { + path: PanelPath; + hasMisconfigurationFindings: boolean; + hasVulnerabilitiesFindings: boolean; + }; +} + +// Type guard to check if the panel is a CspFlyoutPanelProps +function isCspFlyoutPanelProps( + panelLeft: FlyoutPanelProps | undefined +): panelLeft is CspFlyoutPanelProps { + return ( + !!panelLeft?.params?.hasMisconfigurationFindings || + !!panelLeft?.params?.hasVulnerabilitiesFindings + ); +} + export const InsightsTabCsp = memo( ({ name, fieldName }: { name: string; fieldName: 'host.name' | 'user.name' }) => { + const panels = useExpandableFlyoutState(); + + let hasMisconfigurationFindings = false; + let hasVulnerabilitiesFindings = false; + let subTab: string | undefined; + + // Check if panels.left is of type CspFlyoutPanelProps and extract values + if (isCspFlyoutPanelProps(panels.left)) { + hasMisconfigurationFindings = panels.left.params.hasMisconfigurationFindings; + hasVulnerabilitiesFindings = panels.left.params.hasVulnerabilitiesFindings; + subTab = panels.left.params.path?.subTab; + } + + const getDefaultTab = () => { + if (subTab) { + return subTab; + } + + return hasMisconfigurationFindings + ? CspInsightLeftPanelSubTab.MISCONFIGURATIONS + : hasVulnerabilitiesFindings + ? CspInsightLeftPanelSubTab.VULNERABILITIES + : ''; + }; + + const [activeInsightsId, setActiveInsightsId] = useState(getDefaultTab()); + + const insightsButtons: EuiButtonGroupOptionProps[] = useMemo(() => { + const buttons: EuiButtonGroupOptionProps[] = []; + + if (panels.left?.params?.hasMisconfigurationFindings) { + buttons.push({ + id: CspInsightLeftPanelSubTab.MISCONFIGURATIONS, + label: ( + + ), + 'data-test-subj': 'misconfigurationTabDataTestId', + }); + } + + if (panels.left?.params?.hasVulnerabilitiesFindings) { + buttons.push({ + id: CspInsightLeftPanelSubTab.VULNERABILITIES, + label: ( + + ), + 'data-test-subj': 'vulnerabilitiesTabDataTestId', + }); + } + return buttons; + }, [ + panels.left?.params?.hasMisconfigurationFindings, + panels.left?.params?.hasVulnerabilitiesFindings, + ]); + + const onTabChange = (id: string) => { + setActiveInsightsId(id); + }; + + if (insightsButtons.length === 0) { + return null; + } + return ( <> + - + {activeInsightsId === CspInsightLeftPanelSubTab.MISCONFIGURATIONS ? ( + + ) : ( + + )} ); } diff --git a/x-pack/plugins/security_solution/public/cloud_security_posture/components/csp_details/misconfiguration_findings_details_table.tsx b/x-pack/plugins/security_solution/public/cloud_security_posture/components/csp_details/misconfiguration_findings_details_table.tsx index a27f92407fbdd..2cf99abdf4833 100644 --- a/x-pack/plugins/security_solution/public/cloud_security_posture/components/csp_details/misconfiguration_findings_details_table.tsx +++ b/x-pack/plugins/security_solution/public/cloud_security_posture/components/csp_details/misconfiguration_findings_details_table.tsx @@ -18,6 +18,7 @@ import { useNavigateFindings } from '@kbn/cloud-security-posture/src/hooks/use_n import type { CspBenchmarkRuleMetadata } from '@kbn/cloud-security-posture-common/schema/rules/latest'; import { CspEvaluationBadge } from '@kbn/cloud-security-posture'; import { + ENTITY_FLYOUT_MISCONFIGURATION_VIEW_VISITS, NAV_TO_FINDINGS_BY_HOST_NAME_FRPOM_ENTITY_FLYOUT, NAV_TO_FINDINGS_BY_RULE_NAME_FRPOM_ENTITY_FLYOUT, uiMetricService, @@ -57,6 +58,7 @@ const getFindingsStats = (passedFindingsStats: number, failedFindingsStats: numb */ export const MisconfigurationFindingsDetailsTable = memo( ({ fieldName, queryName }: { fieldName: 'host.name' | 'user.name'; queryName: string }) => { + uiMetricService.trackUiMetric(METRIC_TYPE.COUNT, ENTITY_FLYOUT_MISCONFIGURATION_VIEW_VISITS); const { data } = useMisconfigurationFindings({ query: buildEntityFlyoutPreviewQuery(fieldName, queryName), sort: [], diff --git a/x-pack/plugins/security_solution/public/cloud_security_posture/components/csp_details/vulnerabilities_findings_details_table.tsx b/x-pack/plugins/security_solution/public/cloud_security_posture/components/csp_details/vulnerabilities_findings_details_table.tsx new file mode 100644 index 0000000000000..9e3e4b140a9ba --- /dev/null +++ b/x-pack/plugins/security_solution/public/cloud_security_posture/components/csp_details/vulnerabilities_findings_details_table.tsx @@ -0,0 +1,224 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React, { memo, useState } from 'react'; +import type { Criteria, EuiBasicTableColumn } from '@elastic/eui'; +import { EuiSpacer, EuiIcon, EuiPanel, EuiLink, EuiText, EuiBasicTable } from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; +import type { VulnSeverity } from '@kbn/cloud-security-posture-common'; +import { buildEntityFlyoutPreviewQuery } from '@kbn/cloud-security-posture-common'; +import { DistributionBar } from '@kbn/security-solution-distribution-bar'; +import { useNavigateVulnerabilities } from '@kbn/cloud-security-posture/src/hooks/use_navigate_findings'; +import { useVulnerabilitiesFindings } from '@kbn/cloud-security-posture/src/hooks/use_vulnerabilities_findings'; +import type { + CspVulnerabilityFinding, + Vulnerability, +} from '@kbn/cloud-security-posture-common/schema/vulnerabilities/csp_vulnerability_finding'; +import { + getVulnerabilityStats, + CVSScoreBadge, + SeverityStatusBadge, +} from '@kbn/cloud-security-posture'; +import { + ENTITY_FLYOUT_VULNERABILITY_VIEW_VISITS, + NAV_TO_FINDINGS_BY_HOST_NAME_FRPOM_ENTITY_FLYOUT, + uiMetricService, +} from '@kbn/cloud-security-posture-common/utils/ui_metrics'; +import { METRIC_TYPE } from '@kbn/analytics'; + +type VulnerabilitiesFindingDetailFields = Pick< + CspVulnerabilityFinding, + 'vulnerability' | 'resource' +>; + +interface VulnerabilitiesPackage extends Vulnerability { + package: { + name: string; + }; +} + +export const VulnerabilitiesFindingsDetailsTable = memo(({ queryName }: { queryName: string }) => { + uiMetricService.trackUiMetric(METRIC_TYPE.COUNT, ENTITY_FLYOUT_VULNERABILITY_VIEW_VISITS); + const { data } = useVulnerabilitiesFindings({ + query: buildEntityFlyoutPreviewQuery('host.name', queryName), + sort: [], + enabled: true, + pageSize: 1, + }); + + const { CRITICAL = 0, HIGH = 0, MEDIUM = 0, LOW = 0, NONE = 0 } = data?.count || {}; + + const [pageIndex, setPageIndex] = useState(0); + const [pageSize, setPageSize] = useState(10); + + const findingsPagination = (findings: VulnerabilitiesFindingDetailFields[]) => { + let pageOfItems; + + if (!pageIndex && !pageSize) { + pageOfItems = findings; + } else { + const startIndex = pageIndex * pageSize; + pageOfItems = findings?.slice(startIndex, Math.min(startIndex + pageSize, findings?.length)); + } + + return { + pageOfItems, + totalItemCount: findings?.length, + }; + }; + + const { pageOfItems, totalItemCount } = findingsPagination(data?.rows || []); + + const pagination = { + pageIndex, + pageSize, + totalItemCount, + pageSizeOptions: [10, 25, 100], + }; + + const onTableChange = ({ page }: Criteria) => { + if (page) { + const { index, size } = page; + setPageIndex(index); + setPageSize(size); + } + }; + + const navToVulnerabilities = useNavigateVulnerabilities(); + + const navToVulnerabilitiesByName = (name: string, queryField: 'host.name' | 'user.name') => { + navToVulnerabilities({ [queryField]: name }); + }; + + const navToVulnerabilityByVulnerabilityAndResourceId = ( + vulnerabilityId: string, + resourceId: string + ) => { + navToVulnerabilities({ + 'vulnerability.id': vulnerabilityId, + 'resource.id': resourceId, + }); + }; + + const columns: Array> = [ + { + field: 'vulnerability', + name: '', + width: '5%', + render: ( + vulnerability: VulnerabilitiesPackage, + finding: VulnerabilitiesFindingDetailFields + ) => ( + { + navToVulnerabilityByVulnerabilityAndResourceId( + vulnerability?.id, + finding?.resource?.id || '' + ); + }} + > + + + ), + }, + { + field: 'vulnerability', + render: (vulnerability: Vulnerability) => {vulnerability?.id}, + name: i18n.translate( + 'xpack.securitySolution.flyout.left.insights.vulnerability.table.resultColumnName', + { defaultMessage: 'Vulnerability' } + ), + width: '20%', + }, + { + field: 'vulnerability', + render: (vulnerability: Vulnerability) => ( + + + + ), + name: i18n.translate( + 'xpack.securitySolution.flyout.left.insights.vulnerability.table.ruleColumnName', + { defaultMessage: 'CVSS' } + ), + width: '12.5%', + }, + { + field: 'vulnerability', + render: (vulnerability: Vulnerability) => ( + <> + + + + + ), + name: i18n.translate( + 'xpack.securitySolution.flyout.left.insights.vulnerability.table.ruleColumnName', + { defaultMessage: 'Severity' } + ), + width: '12.5%', + }, + { + field: 'vulnerability', + render: (vulnerability: VulnerabilitiesPackage) => ( + {vulnerability?.package?.name} + ), + name: i18n.translate( + 'xpack.securitySolution.flyout.left.insights.vulnerability.table.ruleColumnName', + { defaultMessage: 'Package' } + ), + width: '50%', + }, + ]; + + return ( + <> + + { + uiMetricService.trackUiMetric( + METRIC_TYPE.CLICK, + NAV_TO_FINDINGS_BY_HOST_NAME_FRPOM_ENTITY_FLYOUT + ); + navToVulnerabilitiesByName(queryName, 'host.name'); + }} + > + {i18n.translate('xpack.securitySolution.flyout.left.insights.vulnerability.tableTitle', { + defaultMessage: 'Vulnerability ', + })} + + + + + + + + + ); +}); + +VulnerabilitiesFindingsDetailsTable.displayName = 'VulnerabilitiesFindingsDetailsTable'; diff --git a/x-pack/plugins/security_solution/public/cloud_security_posture/components/index.tsx b/x-pack/plugins/security_solution/public/cloud_security_posture/components/entity_insight.tsx similarity index 60% rename from x-pack/plugins/security_solution/public/cloud_security_posture/components/index.tsx rename to x-pack/plugins/security_solution/public/cloud_security_posture/components/entity_insight.tsx index b4ec54a29a073..7d9027c25a9e0 100644 --- a/x-pack/plugins/security_solution/public/cloud_security_posture/components/index.tsx +++ b/x-pack/plugins/security_solution/public/cloud_security_posture/components/entity_insight.tsx @@ -10,7 +10,10 @@ import { EuiAccordion, EuiHorizontalRule, EuiSpacer, EuiTitle, useEuiTheme } fro import React from 'react'; import { css } from '@emotion/react'; import { FormattedMessage } from '@kbn/i18n-react'; -import { useCspSetupStatusApi } from '@kbn/cloud-security-posture/src/hooks/use_csp_setup_status_api'; +import { useMisconfigurationPreview } from '@kbn/cloud-security-posture/src/hooks/use_misconfiguration_preview'; +import { buildEntityFlyoutPreviewQuery } from '@kbn/cloud-security-posture-common'; +import { useVulnerabilitiesPreview } from '@kbn/cloud-security-posture/src/hooks/use_vulnerabilities_preview'; +import { hasVulnerabilitiesData } from '@kbn/cloud-security-posture'; import { MisconfigurationsPreview } from './misconfiguration/misconfiguration_preview'; import { VulnerabilitiesPreview } from './vulnerabilities/vulnerabilities_preview'; @@ -24,10 +27,37 @@ export const EntityInsight = ({ isPreviewMode?: boolean; }) => { const { euiTheme } = useEuiTheme(); - const getSetupStatus = useCspSetupStatusApi(); - const hasMisconfigurationFindings = getSetupStatus.data?.hasMisconfigurationsFindings; - const hasVulnerabilitiesFindings = getSetupStatus.data?.hasVulnerabilitiesFindings; const insightContent: React.ReactElement[] = []; + + const { data: dataMisconfiguration } = useMisconfigurationPreview({ + query: buildEntityFlyoutPreviewQuery(fieldName, name), + sort: [], + enabled: true, + pageSize: 1, + }); + + const passedFindings = dataMisconfiguration?.count.passed || 0; + const failedFindings = dataMisconfiguration?.count.failed || 0; + + const hasMisconfigurationFindings = passedFindings > 0 || failedFindings > 0; + + const { data } = useVulnerabilitiesPreview({ + query: buildEntityFlyoutPreviewQuery(fieldName, name), + sort: [], + enabled: true, + pageSize: 1, + }); + + const { CRITICAL = 0, HIGH = 0, MEDIUM = 0, LOW = 0, NONE = 0 } = data?.count || {}; + + const hasVulnerabilitiesFindings = hasVulnerabilitiesData({ + critical: CRITICAL, + high: HIGH, + medium: MEDIUM, + low: LOW, + none: NONE, + }); + const isVulnerabilitiesFindingForHost = hasVulnerabilitiesFindings && fieldName === 'host.name'; if (hasMisconfigurationFindings) @@ -37,16 +67,17 @@ export const EntityInsight = ({ ); - if (isVulnerabilitiesFindingForHost) + if (isVulnerabilitiesFindingForHost && hasVulnerabilitiesFindings) insightContent.push( <> - + ); return ( <> - {(hasMisconfigurationFindings || isVulnerabilitiesFindingForHost) && ( + {(hasMisconfigurationFindings || + (isVulnerabilitiesFindingForHost && hasVulnerabilitiesFindings)) && ( <> { - it('renders', () => { - const { queryByTestId } = render(, { - wrapper: TestProviders, + const mockOpenLeftPanel = jest.fn(); + + beforeEach(() => { + (useExpandableFlyoutApi as jest.Mock).mockReturnValue({ openLeftPanel: mockOpenLeftPanel }); + (useVulnerabilitiesPreview as jest.Mock).mockReturnValue({ + data: { count: { CRITICAL: 0, HIGH: 1, MEDIUM: 1, LOW: 0, UNKNOWN: 0 } }, + }); + (useRiskScore as jest.Mock).mockReturnValue({ data: [{ host: { risk: 75 } }] }); + (useMisconfigurationPreview as jest.Mock).mockReturnValue({ + data: { count: { passed: 1, failed: 1 } }, }); + }); + + it('renders', () => { + const { getByTestId } = render( + + + + ); + expect( - queryByTestId('securitySolutionFlyoutInsightsMisconfigurationsContent') + getByTestId('securitySolutionFlyoutInsightsMisconfigurationsTitleLink') ).toBeInTheDocument(); - expect(queryByTestId('noFindingsDataTestSubj')).toBeInTheDocument(); }); }); diff --git a/x-pack/plugins/security_solution/public/cloud_security_posture/components/misconfiguration/misconfiguration_preview.tsx b/x-pack/plugins/security_solution/public/cloud_security_posture/components/misconfiguration/misconfiguration_preview.tsx index e6c3950e81583..a372ca4755fd8 100644 --- a/x-pack/plugins/security_solution/public/cloud_security_posture/components/misconfiguration/misconfiguration_preview.tsx +++ b/x-pack/plugins/security_solution/public/cloud_security_posture/components/misconfiguration/misconfiguration_preview.tsx @@ -17,6 +17,12 @@ import { i18n } from '@kbn/i18n'; import { ExpandablePanel } from '@kbn/security-solution-common'; import { buildEntityFlyoutPreviewQuery } from '@kbn/cloud-security-posture-common'; import { useExpandableFlyoutApi } from '@kbn/expandable-flyout'; +import { useVulnerabilitiesPreview } from '@kbn/cloud-security-posture/src/hooks/use_vulnerabilities_preview'; +import { hasVulnerabilitiesData } from '@kbn/cloud-security-posture'; +import { + CspInsightLeftPanelSubTab, + EntityDetailsLeftPanelTab, +} from '../../../flyout/entity_details/shared/components/left_panel/left_panel_header'; import { UserDetailsPanelKey } from '../../../flyout/entity_details/user_details_left'; import { HostDetailsPanelKey } from '../../../flyout/entity_details/host_details_left'; import { useRiskScore } from '../../../entity_analytics/api/hooks/use_risk_score'; @@ -29,7 +35,7 @@ const FIRST_RECORD_PAGINATION = { querySize: 1, }; -const getFindingsStats = (passedFindingsStats: number, failedFindingsStats: number) => { +export const getFindingsStats = (passedFindingsStats: number, failedFindingsStats: number) => { if (passedFindingsStats === 0 && failedFindingsStats === 0) return []; return [ { @@ -55,34 +61,6 @@ const getFindingsStats = (passedFindingsStats: number, failedFindingsStats: numb ]; }; -const MisconfigurationEmptyState = ({ euiTheme }: { euiTheme: EuiThemeComputed<{}> }) => { - return ( - - - - -

    {'-'}

    -
    -
    - - - - - -
    -
    - ); -}; - const MisconfigurationPreviewScore = ({ passedFindings, failedFindings, @@ -136,6 +114,7 @@ export const MisconfigurationsPreview = ({ sort: [], enabled: true, pageSize: 1, + ignore_unavailable: true, }); const isUsingHostName = fieldName === 'host.name'; const passedFindings = data?.count.passed || 0; @@ -144,6 +123,29 @@ export const MisconfigurationsPreview = ({ const { euiTheme } = useEuiTheme(); const hasMisconfigurationFindings = passedFindings > 0 || failedFindings > 0; + const { data: vulnerabilitiesData } = useVulnerabilitiesPreview({ + query: buildEntityFlyoutPreviewQuery('host.name', name), + sort: [], + enabled: true, + pageSize: 1, + }); + + const { + CRITICAL = 0, + HIGH = 0, + MEDIUM = 0, + LOW = 0, + NONE = 0, + } = vulnerabilitiesData?.count || {}; + + const hasVulnerabilitiesFindings = hasVulnerabilitiesData({ + critical: CRITICAL, + high: HIGH, + medium: MEDIUM, + low: LOW, + none: NONE, + }); + const buildFilterQuery = useMemo( () => (isUsingHostName ? buildHostNamesFilter([name]) : buildUserNamesFilter([name])), [isUsingHostName, name] @@ -155,12 +157,17 @@ export const MisconfigurationsPreview = ({ onlyLatest: false, pagination: FIRST_RECORD_PAGINATION, }); + const { data: hostRisk } = riskScoreState; + const riskData = hostRisk?.[0]; + const isRiskScoreExist = isUsingHostName ? !!(riskData as HostRiskScore)?.host.risk : !!(riskData as UserRiskScore)?.user.risk; + const { openLeftPanel } = useExpandableFlyoutApi(); + const goToEntityInsightTab = useCallback(() => { openLeftPanel({ id: isUsingHostName ? HostDetailsPanelKey : UserDetailsPanelKey, @@ -169,16 +176,27 @@ export const MisconfigurationsPreview = ({ name, isRiskScoreExist, hasMisconfigurationFindings, - path: { tab: 'csp_insights' }, + hasVulnerabilitiesFindings, + path: { + tab: EntityDetailsLeftPanelTab.CSP_INSIGHTS, + subTab: CspInsightLeftPanelSubTab.MISCONFIGURATIONS, + }, } : { user: { name }, isRiskScoreExist, hasMisconfigurationFindings, - path: { tab: 'csp_insights' }, + path: { tab: EntityDetailsLeftPanelTab.CSP_INSIGHTS }, }, }); - }, [hasMisconfigurationFindings, isRiskScoreExist, isUsingHostName, name, openLeftPanel]); + }, [ + hasMisconfigurationFindings, + hasVulnerabilitiesFindings, + isRiskScoreExist, + isUsingHostName, + name, + openLeftPanel, + ]); const link = useMemo( () => !isPreviewMode @@ -216,15 +234,12 @@ export const MisconfigurationsPreview = ({ data-test-subj={'securitySolutionFlyoutInsightsMisconfigurations'} > - {hasMisconfigurationFindings ? ( - - ) : ( - - )} + + diff --git a/x-pack/plugins/security_solution/public/cloud_security_posture/components/vulnerabilities/vulnerabilities_preview.test.tsx b/x-pack/plugins/security_solution/public/cloud_security_posture/components/vulnerabilities/vulnerabilities_preview.test.tsx index 0436da3e192b3..cc71d1be2158d 100644 --- a/x-pack/plugins/security_solution/public/cloud_security_posture/components/vulnerabilities/vulnerabilities_preview.test.tsx +++ b/x-pack/plugins/security_solution/public/cloud_security_posture/components/vulnerabilities/vulnerabilities_preview.test.tsx @@ -5,23 +5,44 @@ * 2.0. */ -import { TestProviders } from '../../../common/mock'; -import { render } from '@testing-library/react'; import React from 'react'; +import { render } from '@testing-library/react'; import { VulnerabilitiesPreview } from './vulnerabilities_preview'; +import { useMisconfigurationPreview } from '@kbn/cloud-security-posture/src/hooks/use_misconfiguration_preview'; +import { useVulnerabilitiesPreview } from '@kbn/cloud-security-posture/src/hooks/use_vulnerabilities_preview'; +import { useRiskScore } from '../../../entity_analytics/api/hooks/use_risk_score'; +import { useExpandableFlyoutApi } from '@kbn/expandable-flyout'; +import { TestProviders } from '../../../common/mock/test_providers'; -const mockProps: { hostName: string } = { - hostName: 'testContextID', -}; +// Mock hooks +jest.mock('@kbn/cloud-security-posture/src/hooks/use_misconfiguration_preview'); +jest.mock('@kbn/cloud-security-posture/src/hooks/use_vulnerabilities_preview'); +jest.mock('../../../entity_analytics/api/hooks/use_risk_score'); +jest.mock('@kbn/expandable-flyout'); describe('VulnerabilitiesPreview', () => { - it('renders', () => { - const { queryByTestId } = render(, { - wrapper: TestProviders, + const mockOpenLeftPanel = jest.fn(); + + beforeEach(() => { + (useExpandableFlyoutApi as jest.Mock).mockReturnValue({ openLeftPanel: mockOpenLeftPanel }); + (useVulnerabilitiesPreview as jest.Mock).mockReturnValue({ + data: { count: { CRITICAL: 0, HIGH: 1, MEDIUM: 1, LOW: 0, UNKNOWN: 0 } }, + }); + (useRiskScore as jest.Mock).mockReturnValue({ data: [{ host: { risk: 75 } }] }); + (useMisconfigurationPreview as jest.Mock).mockReturnValue({ + data: { count: { passed: 1, failed: 1 } }, }); + }); + + it('renders', () => { + const { getByTestId } = render( + + + + ); + expect( - queryByTestId('securitySolutionFlyoutInsightsVulnerabilitiesContent') + getByTestId('securitySolutionFlyoutInsightsVulnerabilitiesTitleLink') ).toBeInTheDocument(); - expect(queryByTestId('noVulnerabilitiesDataTestSubj')).toBeInTheDocument(); }); }); diff --git a/x-pack/plugins/security_solution/public/cloud_security_posture/components/vulnerabilities/vulnerabilities_preview.tsx b/x-pack/plugins/security_solution/public/cloud_security_posture/components/vulnerabilities/vulnerabilities_preview.tsx index 6e30d39fc98a6..eef778b1e6f0c 100644 --- a/x-pack/plugins/security_solution/public/cloud_security_posture/components/vulnerabilities/vulnerabilities_preview.tsx +++ b/x-pack/plugins/security_solution/public/cloud_security_posture/components/vulnerabilities/vulnerabilities_preview.tsx @@ -5,125 +5,30 @@ * 2.0. */ -import React from 'react'; +import React, { useCallback, useMemo } from 'react'; import { css } from '@emotion/react'; import type { EuiThemeComputed } from '@elastic/eui'; import { EuiFlexGroup, EuiFlexItem, EuiSpacer, EuiText, useEuiTheme, EuiTitle } from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n-react'; import { DistributionBar } from '@kbn/security-solution-distribution-bar'; import { useVulnerabilitiesPreview } from '@kbn/cloud-security-posture/src/hooks/use_vulnerabilities_preview'; -import { i18n } from '@kbn/i18n'; import { ExpandablePanel } from '@kbn/security-solution-common'; import { buildEntityFlyoutPreviewQuery, - VULNERABILITIES_SEVERITY, getAbbreviatedNumber, } from '@kbn/cloud-security-posture-common'; -import { getSeverityStatusColor, getSeverityText } from '@kbn/cloud-security-posture'; +import { getVulnerabilityStats, hasVulnerabilitiesData } from '@kbn/cloud-security-posture'; +import { useExpandableFlyoutApi } from '@kbn/expandable-flyout'; +import { useMisconfigurationPreview } from '@kbn/cloud-security-posture/src/hooks/use_misconfiguration_preview'; +import { EntityDetailsLeftPanelTab } from '../../../flyout/entity_details/shared/components/left_panel/left_panel_header'; +import { HostDetailsPanelKey } from '../../../flyout/entity_details/host_details_left'; +import { useRiskScore } from '../../../entity_analytics/api/hooks/use_risk_score'; +import { RiskScoreEntity } from '../../../../common/entity_analytics/risk_engine'; +import { buildHostNamesFilter } from '../../../../common/search_strategy'; -interface VulnerabilitiesDistributionBarProps { - key: string; - count: number; - color: string; -} - -const getVulnerabilityStats = ( - critical: number, - high: number, - medium: number, - low: number, - none: number -): VulnerabilitiesDistributionBarProps[] => { - const vulnerabilityStats: VulnerabilitiesDistributionBarProps[] = []; - if (critical === 0 && high === 0 && medium === 0 && low === 0 && none === 0) - return vulnerabilityStats; - - if (none > 0) - vulnerabilityStats.push({ - key: i18n.translate( - 'xpack.securitySolution.flyout.right.insights.vulnerabilities.noneVulnerabilitiesText', - { - defaultMessage: getSeverityText(VULNERABILITIES_SEVERITY.UNKNOWN), - } - ), - count: none, - color: getSeverityStatusColor(VULNERABILITIES_SEVERITY.UNKNOWN), - }); - if (low > 0) - vulnerabilityStats.push({ - key: i18n.translate( - 'xpack.securitySolution.flyout.right.insights.vulnerabilities.lowVulnerabilitiesText', - { - defaultMessage: getSeverityText(VULNERABILITIES_SEVERITY.LOW), - } - ), - count: low, - color: getSeverityStatusColor(VULNERABILITIES_SEVERITY.LOW), - }); - - if (medium > 0) - vulnerabilityStats.push({ - key: i18n.translate( - 'xpack.securitySolution.flyout.right.insights.vulnerabilities.mediumVulnerabilitiesText', - { - defaultMessage: getSeverityText(VULNERABILITIES_SEVERITY.MEDIUM), - } - ), - count: medium, - color: getSeverityStatusColor(VULNERABILITIES_SEVERITY.MEDIUM), - }); - if (high > 0) - vulnerabilityStats.push({ - key: i18n.translate( - 'xpack.securitySolution.flyout.right.insights.vulnerabilities.highVulnerabilitiesText', - { - defaultMessage: getSeverityText(VULNERABILITIES_SEVERITY.HIGH), - } - ), - count: high, - color: getSeverityStatusColor(VULNERABILITIES_SEVERITY.HIGH), - }); - if (critical > 0) - vulnerabilityStats.push({ - key: i18n.translate( - 'xpack.securitySolution.flyout.right.insights.vulnerabilities.CriticalVulnerabilitiesText', - { - defaultMessage: getSeverityText(VULNERABILITIES_SEVERITY.CRITICAL), - } - ), - count: critical, - color: getSeverityStatusColor(VULNERABILITIES_SEVERITY.CRITICAL), - }); - - return vulnerabilityStats; -}; - -const VulnerabilitiesEmptyState = ({ euiTheme }: { euiTheme: EuiThemeComputed<{}> }) => { - return ( - - - - -

    {'-'}

    -
    -
    - - - - - -
    -
    - ); +const FIRST_RECORD_PAGINATION = { + cursorStart: 0, + querySize: 1, }; const VulnerabilitiesCount = ({ @@ -159,9 +64,15 @@ const VulnerabilitiesCount = ({ ); }; -export const VulnerabilitiesPreview = ({ hostName }: { hostName: string }) => { +export const VulnerabilitiesPreview = ({ + name, + isPreviewMode, +}: { + name: string; + isPreviewMode?: boolean; +}) => { const { data } = useVulnerabilitiesPreview({ - query: buildEntityFlyoutPreviewQuery('host.name', hostName), + query: buildEntityFlyoutPreviewQuery('host.name', name), sort: [], enabled: true, pageSize: 1, @@ -170,11 +81,77 @@ export const VulnerabilitiesPreview = ({ hostName }: { hostName: string }) => { const { CRITICAL = 0, HIGH = 0, MEDIUM = 0, LOW = 0, NONE = 0 } = data?.count || {}; const totalVulnerabilities = CRITICAL + HIGH + MEDIUM + LOW + NONE; + + const hasVulnerabilitiesFindings = hasVulnerabilitiesData({ + critical: CRITICAL, + high: HIGH, + medium: MEDIUM, + low: LOW, + none: NONE, + }); + const { euiTheme } = useEuiTheme(); - const hasVulnerabilities = totalVulnerabilities > 0; + + const { data: dataMisconfiguration } = useMisconfigurationPreview({ + query: buildEntityFlyoutPreviewQuery('host.name', name), + sort: [], + enabled: true, + pageSize: 1, + }); + + const passedFindings = dataMisconfiguration?.count.passed || 0; + const failedFindings = dataMisconfiguration?.count.failed || 0; + + const hasMisconfigurationFindings = passedFindings > 0 || failedFindings > 0; + + const buildFilterQuery = useMemo(() => buildHostNamesFilter([name]), [name]); + const riskScoreState = useRiskScore({ + riskEntity: RiskScoreEntity.host, + filterQuery: buildFilterQuery, + onlyLatest: false, + pagination: FIRST_RECORD_PAGINATION, + }); + const { data: hostRisk } = riskScoreState; + const riskData = hostRisk?.[0]; + const isRiskScoreExist = riskData?.host.risk; + const { openLeftPanel } = useExpandableFlyoutApi(); + const goToEntityInsightTab = useCallback(() => { + openLeftPanel({ + id: HostDetailsPanelKey, + params: { + name, + isRiskScoreExist, + hasMisconfigurationFindings, + hasVulnerabilitiesFindings, + path: { tab: EntityDetailsLeftPanelTab.CSP_INSIGHTS, subTab: 'vulnerabilitiesTabId' }, + }, + }); + }, [ + hasMisconfigurationFindings, + hasVulnerabilitiesFindings, + isRiskScoreExist, + name, + openLeftPanel, + ]); + const link = useMemo( + () => + !isPreviewMode + ? { + callback: goToEntityInsightTab, + tooltip: ( + + ), + } + : undefined, + [isPreviewMode, goToEntityInsightTab] + ); return ( { /> ), + link, }} data-test-subj={'securitySolutionFlyoutInsightsVulnerabilities'} > - {hasVulnerabilities ? ( - - ) : ( - - )} + - + 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 039860093e423..793ca853598b3 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 @@ -5,7 +5,7 @@ * 2.0. */ -import { cloneDeep } from 'lodash'; +import { cloneDeep, isEmpty } from 'lodash'; import React, { memo, useMemo, useCallback, useState, useEffect } from 'react'; import deepEqual from 'fast-deep-equal'; @@ -125,7 +125,7 @@ export const QueryBar = memo( let dv: DataView; if (isDataView(indexPattern)) { setDataView(indexPattern); - } else if (!isEsql) { + } else if (!isEsql && !isEmpty(indexPattern.title)) { const createDataView = async () => { dv = await data.dataViews.create({ id: indexPattern.title, title: indexPattern.title }); setDataView(dv); diff --git a/x-pack/plugins/security_solution/public/common/lib/telemetry/constants.ts b/x-pack/plugins/security_solution/public/common/lib/telemetry/constants.ts index f42f77f19a0f9..5126d75178f5f 100644 --- a/x-pack/plugins/security_solution/public/common/lib/telemetry/constants.ts +++ b/x-pack/plugins/security_solution/public/common/lib/telemetry/constants.ts @@ -86,6 +86,7 @@ export enum TelemetryEventTypes { EventLogShowSourceEventDateRange = 'Event Log -> Show Source -> Event Date Range', OpenNoteInExpandableFlyoutClicked = 'Open Note In Expandable Flyout Clicked', AddNoteFromExpandableFlyoutClicked = 'Add Note From Expandable Flyout Clicked', + PreviewRule = 'Preview rule', } export enum ML_JOB_TELEMETRY_STATUS { diff --git a/x-pack/plugins/security_solution/public/common/lib/telemetry/events/preview_rule/index.ts b/x-pack/plugins/security_solution/public/common/lib/telemetry/events/preview_rule/index.ts new file mode 100644 index 0000000000000..12d721c45e2c0 --- /dev/null +++ b/x-pack/plugins/security_solution/public/common/lib/telemetry/events/preview_rule/index.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 { TelemetryEvent } from '../../types'; +import { TelemetryEventTypes } from '../../constants'; + +export const previewRuleEvent: TelemetryEvent = { + eventType: TelemetryEventTypes.PreviewRule, + schema: { + ruleType: { + type: 'keyword', + _meta: { + description: 'Rule type', + optional: false, + }, + }, + loggedRequestsEnabled: { + type: 'boolean', + _meta: { + description: 'shows if preview executed with enabled logged requests', + optional: false, + }, + }, + }, +}; diff --git a/x-pack/plugins/security_solution/public/common/lib/telemetry/events/preview_rule/types.ts b/x-pack/plugins/security_solution/public/common/lib/telemetry/events/preview_rule/types.ts new file mode 100644 index 0000000000000..e5523080088fc --- /dev/null +++ b/x-pack/plugins/security_solution/public/common/lib/telemetry/events/preview_rule/types.ts @@ -0,0 +1,20 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ +import type { Type } from '@kbn/securitysolution-io-ts-alerting-types'; + +import type { RootSchema } from '@kbn/core/public'; +import type { TelemetryEventTypes } from '../../constants'; + +export interface PreviewRuleParams { + ruleType: Type; + loggedRequestsEnabled: boolean; +} + +export interface PreviewRuleTelemetryEvent { + eventType: TelemetryEventTypes.PreviewRule; + schema: RootSchema; +} diff --git a/x-pack/plugins/security_solution/public/common/lib/telemetry/events/telemetry_events.ts b/x-pack/plugins/security_solution/public/common/lib/telemetry/events/telemetry_events.ts index d1f9502346a04..a0328099b9ff7 100644 --- a/x-pack/plugins/security_solution/public/common/lib/telemetry/events/telemetry_events.ts +++ b/x-pack/plugins/security_solution/public/common/lib/telemetry/events/telemetry_events.ts @@ -48,6 +48,7 @@ import { addNoteFromExpandableFlyoutClickedEvent, openNoteInExpandableFlyoutClickedEvent, } from './notes'; +import { previewRuleEvent } from './preview_rule'; const mlJobUpdateEvent: TelemetryEvent = { eventType: TelemetryEventTypes.MLJobUpdate, @@ -192,4 +193,5 @@ export const telemetryEvents = [ eventLogShowSourceEventDateRangeEvent, openNoteInExpandableFlyoutClickedEvent, addNoteFromExpandableFlyoutClickedEvent, + previewRuleEvent, ]; diff --git a/x-pack/plugins/security_solution/public/common/lib/telemetry/telemetry_client.mock.ts b/x-pack/plugins/security_solution/public/common/lib/telemetry/telemetry_client.mock.ts index 02342cb4257be..98d6aa64bb9cb 100644 --- a/x-pack/plugins/security_solution/public/common/lib/telemetry/telemetry_client.mock.ts +++ b/x-pack/plugins/security_solution/public/common/lib/telemetry/telemetry_client.mock.ts @@ -42,4 +42,5 @@ export const createTelemetryClientMock = (): jest.Mocked = reportManualRuleRunOpenModal: jest.fn(), reportOpenNoteInExpandableFlyoutClicked: jest.fn(), reportAddNoteFromExpandableFlyoutClicked: jest.fn(), + reportPreviewRule: jest.fn(), }); diff --git a/x-pack/plugins/security_solution/public/common/lib/telemetry/telemetry_client.ts b/x-pack/plugins/security_solution/public/common/lib/telemetry/telemetry_client.ts index 0023064adac69..e09f0a3c2eb66 100644 --- a/x-pack/plugins/security_solution/public/common/lib/telemetry/telemetry_client.ts +++ b/x-pack/plugins/security_solution/public/common/lib/telemetry/telemetry_client.ts @@ -44,6 +44,7 @@ import type { ReportManualRuleRunOpenModalParams, ReportEventLogShowSourceEventDateRangeParams, ReportEventLogFilterByRunTypeParams, + PreviewRuleParams, } from './types'; import { TelemetryEventTypes } from './constants'; @@ -211,4 +212,8 @@ export class TelemetryClient implements TelemetryClientStart { ) => { this.analytics.reportEvent(TelemetryEventTypes.AddNoteFromExpandableFlyoutClicked, params); }; + + public reportPreviewRule = (params: PreviewRuleParams) => { + this.analytics.reportEvent(TelemetryEventTypes.PreviewRule, params); + }; } diff --git a/x-pack/plugins/security_solution/public/common/lib/telemetry/types.ts b/x-pack/plugins/security_solution/public/common/lib/telemetry/types.ts index 49c78dc50feeb..55b91837a2585 100644 --- a/x-pack/plugins/security_solution/public/common/lib/telemetry/types.ts +++ b/x-pack/plugins/security_solution/public/common/lib/telemetry/types.ts @@ -72,6 +72,7 @@ import type { NotesTelemetryEvents, OpenNoteInExpandableFlyoutClickedParams, } from './events/notes/types'; +import type { PreviewRuleParams, PreviewRuleTelemetryEvent } from './events/preview_rule/types'; export * from './events/ai_assistant/types'; export * from './events/alerts_grouping/types'; @@ -91,6 +92,7 @@ export type { export * from './events/document_details/types'; export * from './events/manual_rule_run/types'; export * from './events/event_log/types'; +export * from './events/preview_rule/types'; export interface TelemetryServiceSetupParams { analytics: AnalyticsServiceSetup; @@ -136,6 +138,7 @@ export type TelemetryEventParams = | OnboardingHubStepLinkClickedParams | ReportManualRuleRunTelemetryEventParams | ReportEventLogTelemetryEventParams + | PreviewRuleParams | NotesTelemetryEventParams; export interface TelemetryClientStart { @@ -194,6 +197,9 @@ export interface TelemetryClientStart { // new notes reportOpenNoteInExpandableFlyoutClicked(params: OpenNoteInExpandableFlyoutClickedParams): void; reportAddNoteFromExpandableFlyoutClicked(params: AddNoteFromExpandableFlyoutClickedParams): void; + + // preview rule + reportPreviewRule(params: PreviewRuleParams): void; } export type TelemetryEvent = @@ -221,4 +227,5 @@ export type TelemetryEvent = | OnboardingHubTelemetryEvent | ManualRuleRunTelemetryEvent | EventLogTelemetryEvent + | PreviewRuleTelemetryEvent | NotesTelemetryEvents; diff --git a/x-pack/plugins/security_solution/public/detection_engine/rule_creation/components/step_rule_actions/index.tsx b/x-pack/plugins/security_solution/public/detection_engine/rule_creation/components/step_rule_actions/index.tsx index 9e6346cc9040e..06168ce97a2c7 100644 --- a/x-pack/plugins/security_solution/public/detection_engine/rule_creation/components/step_rule_actions/index.tsx +++ b/x-pack/plugins/security_solution/public/detection_engine/rule_creation/components/step_rule_actions/index.tsx @@ -16,6 +16,7 @@ import type { } from '@kbn/triggers-actions-ui-plugin/public'; import { UseArray } from '@kbn/es-ui-shared-plugin/static/forms/hook_form_lib'; import type { Type } from '@kbn/securitysolution-io-ts-alerting-types'; +import { useIsExperimentalFeatureEnabled } from '../../../../common/hooks/use_experimental_features'; import { shouldShowResponseActions } from '../../../../../common/detection_engine/utils'; import type { RuleObjectId } from '../../../../../common/api/detection_engine/model/rule_schema'; import { ResponseActionsForm } from '../../../rule_response_actions/response_actions_form'; @@ -84,7 +85,9 @@ const StepRuleActionsComponent: FC = ({ const { services: { application }, } = useKibana(); - + const automatedResponseActionsForAllRulesEnabled = useIsExperimentalFeatureEnabled( + 'automatedResponseActionsForAllRulesEnabled' + ); const displayActionsOptions = useMemo( () => ( <> @@ -102,7 +105,7 @@ const StepRuleActionsComponent: FC = ({ [actionMessageParams, summaryActionMessageParams] ); const displayResponseActionsOptions = useMemo(() => { - if (shouldShowResponseActions(ruleType)) { + if (shouldShowResponseActions(ruleType, automatedResponseActionsForAllRulesEnabled)) { return ( {ResponseActionsForm} @@ -110,7 +113,7 @@ const StepRuleActionsComponent: FC = ({ ); } return null; - }, [ruleType]); + }, [automatedResponseActionsForAllRulesEnabled, ruleType]); // only display the actions dropdown if the user has "read" privileges for actions const displayActionsDropDown = useMemo(() => { return application.capabilities.actions.show ? ( diff --git a/x-pack/plugins/security_solution/public/detection_engine/rule_creation_ui/components/rule_preview/index.test.tsx b/x-pack/plugins/security_solution/public/detection_engine/rule_creation_ui/components/rule_preview/index.test.tsx index 4ebb460177476..25d5b90d5408a 100644 --- a/x-pack/plugins/security_solution/public/detection_engine/rule_creation_ui/components/rule_preview/index.test.tsx +++ b/x-pack/plugins/security_solution/public/detection_engine/rule_creation_ui/components/rule_preview/index.test.tsx @@ -23,7 +23,6 @@ import { stepDefineDefaultValue, } from '../../../../detections/pages/detection_engine/rules/utils'; import { usePreviewInvocationCount } from './use_preview_invocation_count'; -import { useIsExperimentalFeatureEnabled } from '../../../../common/hooks/use_experimental_features'; jest.mock('../../../../common/lib/kibana'); jest.mock('./use_preview_route'); @@ -40,7 +39,6 @@ jest.mock('../../../../common/hooks/use_experimental_features', () => ({ useIsExperimentalFeatureEnabled: jest.fn(), })); -const useIsExperimentalFeatureEnabledMock = useIsExperimentalFeatureEnabled as jest.Mock; // rule types that do not support logged requests const doNotSupportLoggedRequests: Type[] = [ 'threshold', @@ -114,8 +112,6 @@ describe('PreviewQuery', () => { }); (usePreviewInvocationCount as jest.Mock).mockReturnValue({ invocationCount: 500 }); - - useIsExperimentalFeatureEnabledMock.mockReturnValue(true); }); afterEach(() => { @@ -172,23 +168,6 @@ describe('PreviewQuery', () => { }); }); - supportLoggedRequests.forEach((ruleType) => { - test(`does not render "Show Elasticsearch requests" for ${ruleType} rule type when feature is disabled`, () => { - useIsExperimentalFeatureEnabledMock.mockReturnValue(false); - - render( - - - - ); - - expect(screen.queryByTestId('show-elasticsearch-requests')).toBeNull(); - }); - }); - doNotSupportLoggedRequests.forEach((ruleType) => { test(`does not render "Show Elasticsearch requests" for ${ruleType} rule type`, () => { render( diff --git a/x-pack/plugins/security_solution/public/detection_engine/rule_creation_ui/components/rule_preview/index.tsx b/x-pack/plugins/security_solution/public/detection_engine/rule_creation_ui/components/rule_preview/index.tsx index 2a86600d94e7a..f941cad91d3a4 100644 --- a/x-pack/plugins/security_solution/public/detection_engine/rule_creation_ui/components/rule_preview/index.tsx +++ b/x-pack/plugins/security_solution/public/detection_engine/rule_creation_ui/components/rule_preview/index.tsx @@ -40,7 +40,6 @@ import type { TimeframePreviewOptions, } from '../../../../detections/pages/detection_engine/rules/types'; import { usePreviewInvocationCount } from './use_preview_invocation_count'; -import { useIsExperimentalFeatureEnabled } from '../../../../common/hooks/use_experimental_features'; export const REASONABLE_INVOCATION_COUNT = 200; @@ -90,8 +89,6 @@ const RulePreviewComponent: React.FC = ({ const { indexPattern, ruleType } = defineRuleData; const { spaces } = useKibana().services; - const isLoggingRequestsFeatureEnabled = useIsExperimentalFeatureEnabled('loggingRequestsEnabled'); - const [spaceId, setSpaceId] = useState(''); useEffect(() => { if (spaces) { @@ -282,8 +279,7 @@ const RulePreviewComponent: React.FC = ({
    - {isLoggingRequestsFeatureEnabled && - RULE_TYPES_SUPPORTING_LOGGED_REQUESTS.includes(ruleType) ? ( + {RULE_TYPES_SUPPORTING_LOGGED_REQUESTS.includes(ruleType) ? ( diff --git a/x-pack/plugins/security_solution/public/detection_engine/rule_creation_ui/components/rule_preview/use_preview_rule.ts b/x-pack/plugins/security_solution/public/detection_engine/rule_creation_ui/components/rule_preview/use_preview_rule.ts index 05c3b9fe10299..018e2602aa170 100644 --- a/x-pack/plugins/security_solution/public/detection_engine/rule_creation_ui/components/rule_preview/use_preview_rule.ts +++ b/x-pack/plugins/security_solution/public/detection_engine/rule_creation_ui/components/rule_preview/use_preview_rule.ts @@ -12,7 +12,7 @@ import type { RuleCreateProps, RulePreviewResponse, } from '../../../../../common/api/detection_engine'; - +import { useKibana } from '../../../../common/lib/kibana'; import { previewRule } from '../../../rule_management/api/api'; import { transformOutput } from '../../../../detections/containers/detection_engine/rules/transforms'; import type { TimeframePreviewOptions } from '../../../../detections/pages/detection_engine/rules/types'; @@ -37,6 +37,7 @@ export const usePreviewRule = ({ const [isLoading, setIsLoading] = useState(false); const { addError } = useAppToasts(); const { invocationCount, interval, from } = usePreviewInvocationCount({ timeframeOptions }); + const { telemetry } = useKibana().services; const timeframeEnd = useMemo( () => timeframeOptions.timeframeEnd.toISOString(), @@ -57,6 +58,10 @@ export const usePreviewRule = ({ const createPreviewId = async () => { if (rule != null) { try { + telemetry.reportPreviewRule({ + loggedRequestsEnabled: enableLoggedRequests ?? false, + ruleType: rule.type, + }); setIsLoading(true); const previewRuleResponse = await previewRule({ rule: { @@ -90,7 +95,16 @@ export const usePreviewRule = ({ isSubscribed = false; abortCtrl.abort(); }; - }, [rule, addError, invocationCount, from, interval, timeframeEnd, enableLoggedRequests]); + }, [ + rule, + addError, + invocationCount, + from, + interval, + timeframeEnd, + enableLoggedRequests, + telemetry, + ]); return { isLoading, response, rule, setRule }; }; diff --git a/x-pack/plugins/security_solution/public/detection_engine/rule_creation_ui/components/step_define_rule/translations.tsx b/x-pack/plugins/security_solution/public/detection_engine/rule_creation_ui/components/step_define_rule/translations.tsx index 7d7bb9c4a9253..b212aa7c67dd4 100644 --- a/x-pack/plugins/security_solution/public/detection_engine/rule_creation_ui/components/step_define_rule/translations.tsx +++ b/x-pack/plugins/security_solution/public/detection_engine/rule_creation_ui/components/step_define_rule/translations.tsx @@ -205,15 +205,15 @@ export const THRESHOLD_SUPPRESSION_PER_RULE_EXECUTION_WARNING = i18n.translate( export const getEnableThresholdSuppressionLabel = (fields: string[] | undefined) => fields?.length ? ( {fields.join(', ')}
    }} /> ) : ( i18n.translate( - 'xpack.securitySolution.detectionEngine.createRule.stepDefineRule.enableThresholdSuppressionLabel', + 'xpack.securitySolution.detectionEngine.createRule.stepDefineRule.ga.enableThresholdSuppressionLabel', { - defaultMessage: 'Suppress alerts (Technical Preview)', + defaultMessage: 'Suppress alerts', } ) ); diff --git a/x-pack/plugins/security_solution/public/detection_engine/rule_details_ui/pages/rule_details/execution_log_table/__snapshots__/execution_log_search_bar.test.tsx.snap b/x-pack/plugins/security_solution/public/detection_engine/rule_details_ui/pages/rule_details/execution_log_table/__snapshots__/execution_log_search_bar.test.tsx.snap index 009e6dcc58ace..4f5e9954cced0 100644 --- a/x-pack/plugins/security_solution/public/detection_engine/rule_details_ui/pages/rule_details/execution_log_table/__snapshots__/execution_log_search_bar.test.tsx.snap +++ b/x-pack/plugins/security_solution/public/detection_engine/rule_details_ui/pages/rule_details/execution_log_table/__snapshots__/execution_log_search_bar.test.tsx.snap @@ -13,6 +13,20 @@ exports[`ExecutionLogSearchBar snapshots renders correctly against snapshot 1`] + + + diff --git a/x-pack/plugins/security_solution/public/detection_engine/rule_details_ui/pages/rule_details/execution_log_table/execution_log_search_bar.tsx b/x-pack/plugins/security_solution/public/detection_engine/rule_details_ui/pages/rule_details/execution_log_table/execution_log_search_bar.tsx index db43b104ec713..3c70fa7c33c9c 100644 --- a/x-pack/plugins/security_solution/public/detection_engine/rule_details_ui/pages/rule_details/execution_log_table/execution_log_search_bar.tsx +++ b/x-pack/plugins/security_solution/public/detection_engine/rule_details_ui/pages/rule_details/execution_log_table/execution_log_search_bar.tsx @@ -20,7 +20,6 @@ import { } from '../../../../../../common/detection_engine/rule_management/execution_log'; import { ExecutionStatusFilter, ExecutionRunTypeFilter } from '../../../../rule_monitoring'; -import { useIsExperimentalFeatureEnabled } from '../../../../../common/hooks/use_experimental_features'; import * as i18n from './translations'; export const EXECUTION_LOG_SCHEMA_MAPPING = { @@ -75,7 +74,6 @@ export const ExecutionLogSearchBar = React.memo( }, [onSearch] ); - const isManualRuleRunEnabled = useIsExperimentalFeatureEnabled('manualRuleRunEnabled'); return ( @@ -93,15 +91,14 @@ export const ExecutionLogSearchBar = React.memo( - {isManualRuleRunEnabled && ( - - - - )} + + + + = ({ timelines, telemetry, } = useKibana().services; - const isManualRuleRunEnabled = useIsExperimentalFeatureEnabled('manualRuleRunEnabled'); const { [RuleDetailTabs.executionResults]: { @@ -473,15 +470,10 @@ const ExecutionLogTableComponent: React.FC = ({ ); const executionLogColumns = useMemo(() => { - const columns = [...EXECUTION_LOG_COLUMNS].filter((item) => { - if ('field' in item) { - return item.field === 'type' ? isManualRuleRunEnabled : true; - } - return true; - }); + const columns = [...EXECUTION_LOG_COLUMNS]; let messageColumnWidth = 50; - if (showSourceEventTimeRange && isManualRuleRunEnabled) { + if (showSourceEventTimeRange) { columns.push(...getSourceEventTimeRangeColumns()); messageColumnWidth = 30; } @@ -506,7 +498,6 @@ const ExecutionLogTableComponent: React.FC = ({ return columns; }, [ - isManualRuleRunEnabled, actions, docLinks, showMetricColumns, @@ -583,14 +574,12 @@ const ExecutionLogTableComponent: React.FC = ({ updatedAt: dataUpdatedAt, })} - {isManualRuleRunEnabled && ( - - )} + {i18n.MANUAL_RULE_RUN_MODAL_TITLE} - + } diff --git a/x-pack/plugins/security_solution/public/detection_engine/rule_gaps/components/rule_backfills_info/index.tsx b/x-pack/plugins/security_solution/public/detection_engine/rule_gaps/components/rule_backfills_info/index.tsx index 2bacc44b15a76..2a0981e2f5259 100644 --- a/x-pack/plugins/security_solution/public/detection_engine/rule_gaps/components/rule_backfills_info/index.tsx +++ b/x-pack/plugins/security_solution/public/detection_engine/rule_gaps/components/rule_backfills_info/index.tsx @@ -25,9 +25,8 @@ import { hasUserCRUDPermission } from '../../../../common/utils/privileges'; import { useUserData } from '../../../../detections/components/user_info'; import { getBackfillRowsFromResponse } from './utils'; import { HeaderSection } from '../../../../common/components/header_section'; -import { useIsExperimentalFeatureEnabled } from '../../../../common/hooks/use_experimental_features'; import { TableHeaderTooltipCell } from '../../../rule_management_ui/components/rules_table/table_header_tooltip_cell'; -import { TECHNICAL_PREVIEW, TECHNICAL_PREVIEW_TOOLTIP } from '../../../../common/translations'; +import { BETA, BETA_TOOLTIP } from '../../../../common/translations'; import { useKibana } from '../../../../common/lib/kibana'; const DEFAULT_PAGE_SIZE = 10; @@ -143,26 +142,16 @@ const getBackfillsTableColumns = (hasCRUDPermissions: boolean) => { }; export const RuleBackfillsInfo = React.memo<{ ruleId: string }>(({ ruleId }) => { - const isManualRuleRunEnabled = useIsExperimentalFeatureEnabled('manualRuleRunEnabled'); const [pageIndex, setPageIndex] = useState(0); const [pageSize, setPageSize] = useState(DEFAULT_PAGE_SIZE); const [{ canUserCRUD }] = useUserData(); const hasCRUDPermissions = hasUserCRUDPermission(canUserCRUD); const { timelines } = useKibana().services; - const { data, isLoading, isError, refetch, dataUpdatedAt } = useFindBackfillsForRules( - { - ruleIds: [ruleId], - page: pageIndex + 1, - perPage: pageSize, - }, - { - enabled: isManualRuleRunEnabled, - } - ); - - if (!isManualRuleRunEnabled) { - return null; - } + const { data, isLoading, isError, refetch, dataUpdatedAt } = useFindBackfillsForRules({ + ruleIds: [ruleId], + page: pageIndex + 1, + perPage: pageSize, + }); const backfills: BackfillRow[] = getBackfillRowsFromResponse(data?.data ?? []); @@ -197,7 +186,7 @@ export const RuleBackfillsInfo = React.memo<{ ruleId: string }>(({ ruleId }) => title={i18n.BACKFILL_TABLE_TITLE} subtitle={i18n.BACKFILL_TABLE_SUBTITLE} /> - + diff --git a/x-pack/plugins/security_solution/public/detection_engine/rule_management/components/rule_details/three_way_diff/comparison_side/comparison_side.tsx b/x-pack/plugins/security_solution/public/detection_engine/rule_management/components/rule_details/three_way_diff/comparison_side/comparison_side.tsx index 9ef207b0bb998..2592469beaabb 100644 --- a/x-pack/plugins/security_solution/public/detection_engine/rule_management/components/rule_details/three_way_diff/comparison_side/comparison_side.tsx +++ b/x-pack/plugins/security_solution/public/detection_engine/rule_management/components/rule_details/three_way_diff/comparison_side/comparison_side.tsx @@ -6,6 +6,7 @@ */ import React, { useState } from 'react'; +import { EuiFlexGroup, EuiTitle } from '@elastic/eui'; import { VersionsPicker } from '../versions_picker/versions_picker'; import type { Version } from '../versions_picker/constants'; import { SelectedVersions } from '../versions_picker/constants'; @@ -17,6 +18,8 @@ import type { import { getSubfieldChanges } from './get_subfield_changes'; import { SubfieldChanges } from './subfield_changes'; import { SideHeader } from '../components/side_header'; +import { ComparisonSideHelpInfo } from './comparison_side_help_info'; +import * as i18n from './translations'; interface ComparisonSideProps { fieldName: FieldName; @@ -43,11 +46,19 @@ export function ComparisonSide({ return ( <> - + + +

    + {i18n.TITLE} + +

    +
    + +
    diff --git a/x-pack/plugins/security_solution/public/detection_engine/rule_management/components/rule_details/three_way_diff/comparison_side/comparison_side_help_info.tsx b/x-pack/plugins/security_solution/public/detection_engine/rule_management/components/rule_details/three_way_diff/comparison_side/comparison_side_help_info.tsx new file mode 100644 index 0000000000000..a2b7e1a360150 --- /dev/null +++ b/x-pack/plugins/security_solution/public/detection_engine/rule_management/components/rule_details/three_way_diff/comparison_side/comparison_side_help_info.tsx @@ -0,0 +1,43 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React from 'react'; +import { useToggle } from 'react-use'; +import { EuiPopover, EuiText, EuiButtonIcon } from '@elastic/eui'; +import { FormattedMessage } from '@kbn/i18n-react'; + +/** + * Theme doesn't expose width variables. Using provided size variables will require + * multiplying it by another magic constant. + * + * 320px width looks + * like a [commonly used width in EUI](https://github.com/search?q=repo%3Aelastic%2Feui%20320&type=code). + */ +const POPOVER_WIDTH = 320; + +export function ComparisonSideHelpInfo(): JSX.Element { + const [isPopoverOpen, togglePopover] = useToggle(false); + + const button = ( + + ); + + return ( + + + + + + ); +} diff --git a/x-pack/plugins/security_solution/public/detection_engine/rule_management/components/rule_details/three_way_diff/comparison_side/translations.ts b/x-pack/plugins/security_solution/public/detection_engine/rule_management/components/rule_details/three_way_diff/comparison_side/translations.ts index d60c78646b5ad..8208892ac298d 100644 --- a/x-pack/plugins/security_solution/public/detection_engine/rule_management/components/rule_details/three_way_diff/comparison_side/translations.ts +++ b/x-pack/plugins/security_solution/public/detection_engine/rule_management/components/rule_details/three_way_diff/comparison_side/translations.ts @@ -7,6 +7,13 @@ import { i18n } from '@kbn/i18n'; +export const TITLE = i18n.translate( + 'xpack.securitySolution.detectionEngine.rules.upgradeRules.comparisonSide.title', + { + defaultMessage: 'Diff view', + } +); + export const NO_CHANGES = i18n.translate( 'xpack.securitySolution.detectionEngine.rules.upgradeRules.comparisonSide.noChangesLabel', { diff --git a/x-pack/plugins/security_solution/public/detection_engine/rule_management/components/rule_details/three_way_diff/components/field_upgrade_conflicts_resolver.tsx b/x-pack/plugins/security_solution/public/detection_engine/rule_management/components/rule_details/three_way_diff/components/field_upgrade_conflicts_resolver.tsx index eeafddfc21f03..a750c163814a0 100644 --- a/x-pack/plugins/security_solution/public/detection_engine/rule_management/components/rule_details/three_way_diff/components/field_upgrade_conflicts_resolver.tsx +++ b/x-pack/plugins/security_solution/public/detection_engine/rule_management/components/rule_details/three_way_diff/components/field_upgrade_conflicts_resolver.tsx @@ -16,18 +16,21 @@ import type { ThreeWayDiff, } from '../../../../../../../common/api/detection_engine'; import { ThreeWayDiffConflict } from '../../../../../../../common/api/detection_engine'; +import type { FieldUpgradeState } from '../../../../model/prebuilt_rule_upgrade'; import { ComparisonSide } from '../comparison_side/comparison_side'; import { FinalSide } from '../final_side/final_side'; import { FieldUpgradeConflictsResolverHeader } from './field_upgrade_conflicts_resolver_header'; interface FieldUpgradeConflictsResolverProps { fieldName: FieldName; + fieldUpgradeState: FieldUpgradeState; fieldThreeWayDiff: RuleFieldsDiff[FieldName]; finalDiffableRule: DiffableRule; } export function FieldUpgradeConflictsResolver({ fieldName, + fieldUpgradeState, fieldThreeWayDiff, finalDiffableRule, }: FieldUpgradeConflictsResolverProps): JSX.Element { @@ -37,7 +40,12 @@ export function FieldUpgradeConflictsResolver } + header={ + + } initialIsOpen={hasConflict} data-test-subj="ruleUpgradePerFieldDiff" > diff --git a/x-pack/plugins/security_solution/public/detection_engine/rule_management/components/rule_details/three_way_diff/components/field_upgrade_conflicts_resolver_header.tsx b/x-pack/plugins/security_solution/public/detection_engine/rule_management/components/rule_details/three_way_diff/components/field_upgrade_conflicts_resolver_header.tsx index 2821a0a179b91..a096f025873a5 100644 --- a/x-pack/plugins/security_solution/public/detection_engine/rule_management/components/rule_details/three_way_diff/components/field_upgrade_conflicts_resolver_header.tsx +++ b/x-pack/plugins/security_solution/public/detection_engine/rule_management/components/rule_details/three_way_diff/components/field_upgrade_conflicts_resolver_header.tsx @@ -7,19 +7,27 @@ import React from 'react'; import { camelCase, startCase } from 'lodash'; -import { EuiTitle } from '@elastic/eui'; +import { EuiFlexGroup, EuiTitle } from '@elastic/eui'; import { fieldToDisplayNameMap } from '../../diff_components/translations'; +import type { FieldUpgradeState } from '../../../../model/prebuilt_rule_upgrade'; +import { FieldUpgradeStateInfo } from './field_upgrade_state_info'; interface FieldUpgradeConflictsResolverHeaderProps { fieldName: string; + fieldUpgradeState: FieldUpgradeState; } export function FieldUpgradeConflictsResolverHeader({ fieldName, + fieldUpgradeState, }: FieldUpgradeConflictsResolverHeaderProps): JSX.Element { return ( - -
    {fieldToDisplayNameMap[fieldName] ?? startCase(camelCase(fieldName))}
    -
    + + +
    {fieldToDisplayNameMap[fieldName] ?? startCase(camelCase(fieldName))}
    +
    + + +
    ); } diff --git a/x-pack/plugins/security_solution/public/detection_engine/rule_management/components/rule_details/three_way_diff/components/field_upgrade_state_info/field_upgrade_state_info.tsx b/x-pack/plugins/security_solution/public/detection_engine/rule_management/components/rule_details/three_way_diff/components/field_upgrade_state_info/field_upgrade_state_info.tsx new file mode 100644 index 0000000000000..c49fc18e2c6ba --- /dev/null +++ b/x-pack/plugins/security_solution/public/detection_engine/rule_management/components/rule_details/three_way_diff/components/field_upgrade_state_info/field_upgrade_state_info.tsx @@ -0,0 +1,55 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React from 'react'; +import { EuiIcon, EuiText } from '@elastic/eui'; +import { FieldUpgradeState } from '../../../../../model/prebuilt_rule_upgrade'; +import * as i18n from './translations'; + +interface FieldUpgradeStateInfoProps { + state: FieldUpgradeState; +} + +export function FieldUpgradeStateInfo({ state }: FieldUpgradeStateInfoProps): JSX.Element { + switch (state) { + case FieldUpgradeState.Accepted: + return ( + <> + + +  {i18n.UPDATE_ACCEPTED} + {i18n.SEPARATOR} + {i18n.UPDATE_ACCEPTED_DESCRIPTION} + + + ); + + case FieldUpgradeState.SolvableConflict: + return ( + <> + + +  {i18n.SOLVABLE_CONFLICT} + {i18n.SEPARATOR} + {i18n.SOLVABLE_CONFLICT_DESCRIPTION} + + + ); + + case FieldUpgradeState.NonSolvableConflict: + return ( + <> + + +  {i18n.NON_SOLVABLE_CONFLICT} + {i18n.SEPARATOR} + {i18n.NON_SOLVABLE_CONFLICT_DESCRIPTION} + + + ); + } +} diff --git a/x-pack/plugins/security_solution/public/detection_engine/rule_management/components/rule_details/three_way_diff/components/field_upgrade_state_info/index.ts b/x-pack/plugins/security_solution/public/detection_engine/rule_management/components/rule_details/three_way_diff/components/field_upgrade_state_info/index.ts new file mode 100644 index 0000000000000..69915cc64cdcc --- /dev/null +++ b/x-pack/plugins/security_solution/public/detection_engine/rule_management/components/rule_details/three_way_diff/components/field_upgrade_state_info/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 './field_upgrade_state_info'; diff --git a/x-pack/plugins/security_solution/public/detection_engine/rule_management/components/rule_details/three_way_diff/components/field_upgrade_state_info/translations.tsx b/x-pack/plugins/security_solution/public/detection_engine/rule_management/components/rule_details/three_way_diff/components/field_upgrade_state_info/translations.tsx new file mode 100644 index 0000000000000..36349b5029a87 --- /dev/null +++ b/x-pack/plugins/security_solution/public/detection_engine/rule_management/components/rule_details/three_way_diff/components/field_upgrade_state_info/translations.tsx @@ -0,0 +1,60 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { i18n } from '@kbn/i18n'; + +export const UPDATE_ACCEPTED = i18n.translate( + 'xpack.securitySolution.detectionEngine.rules.upgradeRules.fieldUpgradeState.updateAccepted', + { + defaultMessage: 'Update accepted', + } +); + +export const UPDATE_ACCEPTED_DESCRIPTION = i18n.translate( + 'xpack.securitySolution.detectionEngine.rules.upgradeRules.fieldUpgradeState.updateAcceptedDescription', + { + defaultMessage: + 'You can still make changes, please review/accept all other conflicts before updating the rule.', + } +); + +export const SOLVABLE_CONFLICT = i18n.translate( + 'xpack.securitySolution.detectionEngine.rules.upgradeRules.fieldUpgradeState.solvableConflict', + { + defaultMessage: 'Solved conflict', + } +); + +export const SOLVABLE_CONFLICT_DESCRIPTION = i18n.translate( + 'xpack.securitySolution.detectionEngine.rules.upgradeRules.fieldUpgradeState.solvableConflictDescription', + { + defaultMessage: + 'We have suggested an update for this modified field, please review before accepting.', + } +); + +export const NON_SOLVABLE_CONFLICT = i18n.translate( + 'xpack.securitySolution.detectionEngine.rules.upgradeRules.fieldUpgradeState.nonSolvableConflict', + { + defaultMessage: 'Solved conflict', + } +); + +export const NON_SOLVABLE_CONFLICT_DESCRIPTION = i18n.translate( + 'xpack.securitySolution.detectionEngine.rules.upgradeRules.fieldUpgradeState.nonSolvableConflictDescription', + { + defaultMessage: + 'We have suggested an update for this modified field, please review before accepting.', + } +); + +export const SEPARATOR = i18n.translate( + 'xpack.securitySolution.detectionEngine.rules.upgradeRules.fieldUpgradeState.separator', + { + defaultMessage: ' - ', + } +); diff --git a/x-pack/plugins/security_solution/public/detection_engine/rule_management/components/rule_details/three_way_diff/components/rule_upgrade_callout/index.ts b/x-pack/plugins/security_solution/public/detection_engine/rule_management/components/rule_details/three_way_diff/components/rule_upgrade_callout/index.ts new file mode 100644 index 0000000000000..75ff48ff541a1 --- /dev/null +++ b/x-pack/plugins/security_solution/public/detection_engine/rule_management/components/rule_details/three_way_diff/components/rule_upgrade_callout/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 './rule_upgrade_callout'; diff --git a/x-pack/plugins/security_solution/public/detection_engine/rule_management/components/rule_details/three_way_diff/components/rule_upgrade_callout/rule_upgrade_callout.tsx b/x-pack/plugins/security_solution/public/detection_engine/rule_management/components/rule_details/three_way_diff/components/rule_upgrade_callout/rule_upgrade_callout.tsx new file mode 100644 index 0000000000000..852ab0c91c58e --- /dev/null +++ b/x-pack/plugins/security_solution/public/detection_engine/rule_management/components/rule_details/three_way_diff/components/rule_upgrade_callout/rule_upgrade_callout.tsx @@ -0,0 +1,71 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React, { useMemo } from 'react'; +import { EuiCallOut } from '@elastic/eui'; +import type { RuleUpgradeState } from '../../../../../model/prebuilt_rule_upgrade'; +import { FieldUpgradeState } from '../../../../../model/prebuilt_rule_upgrade'; +import * as i18n from './translations'; + +interface RuleUpgradeCalloutProps { + ruleUpgradeState: RuleUpgradeState; +} + +export function RuleUpgradeCallout({ ruleUpgradeState }: RuleUpgradeCalloutProps): JSX.Element { + const fieldsUpgradeState = ruleUpgradeState.fieldsUpgradeState; + const { numOfNonSolvableConflicts, numOfSolvableConflicts } = useMemo(() => { + let numOfFieldsWithNonSolvableConflicts = 0; + let numOfFieldsWithSolvableConflicts = 0; + + for (const fieldName of Object.keys(fieldsUpgradeState)) { + if (fieldsUpgradeState[fieldName] === FieldUpgradeState.NonSolvableConflict) { + numOfFieldsWithNonSolvableConflicts++; + } + + if (fieldsUpgradeState[fieldName] === FieldUpgradeState.SolvableConflict) { + numOfFieldsWithSolvableConflicts++; + } + } + + return { + numOfNonSolvableConflicts: numOfFieldsWithNonSolvableConflicts, + numOfSolvableConflicts: numOfFieldsWithSolvableConflicts, + }; + }, [fieldsUpgradeState]); + + if (numOfNonSolvableConflicts > 0) { + return ( + +

    {i18n.RULE_HAS_NON_SOLVABLE_CONFLICTS_DESCRIPTION}

    +
    + ); + } + + if (numOfSolvableConflicts > 0) { + return ( + +

    {i18n.RULE_HAS_SOLVABLE_CONFLICTS_DESCRIPTION}

    +
    + ); + } + + return ( + +

    {i18n.RULE_IS_READY_FOR_UPGRADE_DESCRIPTION}

    +
    + ); +} diff --git a/x-pack/plugins/security_solution/public/detection_engine/rule_management/components/rule_details/three_way_diff/components/rule_upgrade_callout/translations.tsx b/x-pack/plugins/security_solution/public/detection_engine/rule_management/components/rule_details/three_way_diff/components/rule_upgrade_callout/translations.tsx new file mode 100644 index 0000000000000..be9ee761388d0 --- /dev/null +++ b/x-pack/plugins/security_solution/public/detection_engine/rule_management/components/rule_details/three_way_diff/components/rule_upgrade_callout/translations.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 { i18n } from '@kbn/i18n'; + +export const RULE_HAS_NON_SOLVABLE_CONFLICTS = (count: number) => + i18n.translate( + 'xpack.securitySolution.detectionEngine.rules.upgradeRules.fieldUpgradeState.ruleHasNonSolvableConflicts', + { + values: { count }, + defaultMessage: + '{count} of the fields has a unsolved conflict. Please review and modify accordingly.', + } + ); + +export const RULE_HAS_NON_SOLVABLE_CONFLICTS_DESCRIPTION = i18n.translate( + 'xpack.securitySolution.detectionEngine.rules.upgradeRules.fieldUpgradeState.ruleHasNonSolvableConflictsDescription', + { + defaultMessage: + 'Please provide an input for the unsolved conflict. You can also keep the current without the updates, or accept the Elastic update but lose your modifications.', + } +); + +export const RULE_HAS_SOLVABLE_CONFLICTS = (count: number) => + i18n.translate( + 'xpack.securitySolution.detectionEngine.rules.upgradeRules.fieldUpgradeState.ruleHasSolvableConflicts', + { + values: { count }, + defaultMessage: + '{count} of the fields has an update conflict, please review the suggested update being updating.', + } + ); + +export const RULE_HAS_SOLVABLE_CONFLICTS_DESCRIPTION = i18n.translate( + 'xpack.securitySolution.detectionEngine.rules.upgradeRules.fieldUpgradeState.ruleHasSolvableConflictsDescription', + { + defaultMessage: + 'Please review the suggested updated version before accepting the update. You can edit and then save the field if you wish to change it.', + } +); + +export const RULE_IS_READY_FOR_UPGRADE = i18n.translate( + 'xpack.securitySolution.detectionEngine.rules.upgradeRules.fieldUpgradeState.ruleIsReadyForUpgrade', + { + defaultMessage: 'The update is ready to be applied.', + } +); + +export const RULE_IS_READY_FOR_UPGRADE_DESCRIPTION = i18n.translate( + 'xpack.securitySolution.detectionEngine.rules.upgradeRules.fieldUpgradeState.ruleIsReadyForUpgradeDescription', + { + defaultMessage: 'All conflicts have now been reviewed and solved please update the rule.', + } +); diff --git a/x-pack/plugins/security_solution/public/detection_engine/rule_management/components/rule_details/three_way_diff/components/rule_upgrade_conflicts_resolver.tsx b/x-pack/plugins/security_solution/public/detection_engine/rule_management/components/rule_details/three_way_diff/components/rule_upgrade_conflicts_resolver.tsx index 57af1b340c776..f60af70c808f5 100644 --- a/x-pack/plugins/security_solution/public/detection_engine/rule_management/components/rule_details/three_way_diff/components/rule_upgrade_conflicts_resolver.tsx +++ b/x-pack/plugins/security_solution/public/detection_engine/rule_management/components/rule_details/three_way_diff/components/rule_upgrade_conflicts_resolver.tsx @@ -9,7 +9,7 @@ import React from 'react'; import type { RuleUpgradeState, SetRuleFieldResolvedValueFn, -} from '../../../../../rule_management_ui/components/rules_table/upgrade_prebuilt_rules_table/use_prebuilt_rules_upgrade_state'; +} from '../../../../model/prebuilt_rule_upgrade'; import { FieldUpgradeConflictsResolver } from './field_upgrade_conflicts_resolver'; interface RuleUpgradeConflictsResolverProps { @@ -31,6 +31,7 @@ export function RuleUpgradeConflictsResolver({ diff --git a/x-pack/plugins/security_solution/public/detection_engine/rule_management/components/rule_details/three_way_diff/components/rule_upgrade_info_bar.tsx b/x-pack/plugins/security_solution/public/detection_engine/rule_management/components/rule_details/three_way_diff/components/rule_upgrade_info_bar.tsx index 7ecde8059cc2f..970f04f383274 100644 --- a/x-pack/plugins/security_solution/public/detection_engine/rule_management/components/rule_details/three_way_diff/components/rule_upgrade_info_bar.tsx +++ b/x-pack/plugins/security_solution/public/detection_engine/rule_management/components/rule_details/three_way_diff/components/rule_upgrade_info_bar.tsx @@ -6,7 +6,7 @@ */ import React from 'react'; -import type { RuleUpgradeState } from '../../../../../rule_management_ui/components/rules_table/upgrade_prebuilt_rules_table/use_prebuilt_rules_upgrade_state'; +import type { RuleUpgradeState } from '../../../../model/prebuilt_rule_upgrade'; import { UtilityBar, UtilityBarGroup, diff --git a/x-pack/plugins/security_solution/public/detection_engine/rule_management/components/rule_details/three_way_diff/components/translations.tsx b/x-pack/plugins/security_solution/public/detection_engine/rule_management/components/rule_details/three_way_diff/components/translations.tsx index 620b3ac1c0ba8..27172cb98755c 100644 --- a/x-pack/plugins/security_solution/public/detection_engine/rule_management/components/rule_details/three_way_diff/components/translations.tsx +++ b/x-pack/plugins/security_solution/public/detection_engine/rule_management/components/rule_details/three_way_diff/components/translations.tsx @@ -11,23 +11,21 @@ import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n-react'; import { useKibana } from '../../../../../../common/lib/kibana/kibana_react'; -export const NUM_OF_FIELDS_WITH_UPDATES = (count: number) => - i18n.translate( - 'xpack.securitySolution.detectionEngine.rules.upgradeRules.diffTab.fieldsWithUpdates', - { - values: { count }, - defaultMessage: 'Upgrade has {count} {count, plural, one {field} other {fields}}', - } - ); +export const NUM_OF_FIELDS_WITH_UPDATES = (count: number) => ( + {count} }} + /> +); -export const NUM_OF_CONFLICTS = (count: number) => - i18n.translate( - 'xpack.securitySolution.detectionEngine.rules.upgradeRules.diffTab.numOfConflicts', - { - values: { count }, - defaultMessage: '{count} {count, plural, one {conflict} other {conflicts}}', - } - ); +export const NUM_OF_CONFLICTS = (count: number) => ( + {count} }} + /> +); const UPGRADE_RULES_DOCS_LINK = i18n.translate( 'xpack.securitySolution.detectionEngine.rules.upgradeRules.updateYourRulesDocsLink', diff --git a/x-pack/plugins/security_solution/public/detection_engine/rule_management/components/rule_details/three_way_diff/final_side/final_side.tsx b/x-pack/plugins/security_solution/public/detection_engine/rule_management/components/rule_details/three_way_diff/final_side/final_side.tsx index 0685d064b32d0..83190015ebc6d 100644 --- a/x-pack/plugins/security_solution/public/detection_engine/rule_management/components/rule_details/three_way_diff/final_side/final_side.tsx +++ b/x-pack/plugins/security_solution/public/detection_engine/rule_management/components/rule_details/three_way_diff/final_side/final_side.tsx @@ -22,9 +22,9 @@ export function FinalSide({ fieldName, finalDiffableRule }: FinalSideProps): JSX return ( <> - +

    - {i18n.UPGRADED_VERSION} + {i18n.FINAL_UPDATE}

    diff --git a/x-pack/plugins/security_solution/public/detection_engine/rule_management/components/rule_details/three_way_diff/final_side/translations.ts b/x-pack/plugins/security_solution/public/detection_engine/rule_management/components/rule_details/three_way_diff/final_side/translations.ts index aa9b4885a964d..8f6a10b5681be 100644 --- a/x-pack/plugins/security_solution/public/detection_engine/rule_management/components/rule_details/three_way_diff/final_side/translations.ts +++ b/x-pack/plugins/security_solution/public/detection_engine/rule_management/components/rule_details/three_way_diff/final_side/translations.ts @@ -7,9 +7,9 @@ import { i18n } from '@kbn/i18n'; -export const UPGRADED_VERSION = i18n.translate( - 'xpack.securitySolution.detectionEngine.rules.upgradeRules.upgradedVersion', +export const FINAL_UPDATE = i18n.translate( + 'xpack.securitySolution.detectionEngine.rules.upgradeRules.finalUpdate', { - defaultMessage: 'Upgraded version', + defaultMessage: 'Final update', } ); diff --git a/x-pack/plugins/security_solution/public/detection_engine/rule_management/components/rule_details/three_way_diff/rule_upgrade_conflicts_resolver_tab.tsx b/x-pack/plugins/security_solution/public/detection_engine/rule_management/components/rule_details/three_way_diff/rule_upgrade_conflicts_resolver_tab.tsx index 10823b8045c96..547cd23c7e86e 100644 --- a/x-pack/plugins/security_solution/public/detection_engine/rule_management/components/rule_details/three_way_diff/rule_upgrade_conflicts_resolver_tab.tsx +++ b/x-pack/plugins/security_solution/public/detection_engine/rule_management/components/rule_details/three_way_diff/rule_upgrade_conflicts_resolver_tab.tsx @@ -10,9 +10,10 @@ import { EuiSpacer } from '@elastic/eui'; import type { RuleUpgradeState, SetRuleFieldResolvedValueFn, -} from '../../../../rule_management_ui/components/rules_table/upgrade_prebuilt_rules_table/use_prebuilt_rules_upgrade_state'; +} from '../../../model/prebuilt_rule_upgrade'; import { RuleUpgradeInfoBar } from './components/rule_upgrade_info_bar'; import { RuleUpgradeConflictsResolver } from './components/rule_upgrade_conflicts_resolver'; +import { RuleUpgradeCallout } from './components/rule_upgrade_callout'; interface RuleUpgradeConflictsResolverTabProps { ruleUpgradeState: RuleUpgradeState; @@ -28,6 +29,8 @@ export function RuleUpgradeConflictsResolverTab({ + + ; diff --git a/x-pack/plugins/security_solution/public/detection_engine/rule_management/model/prebuilt_rule_upgrade/index.ts b/x-pack/plugins/security_solution/public/detection_engine/rule_management/model/prebuilt_rule_upgrade/index.ts new file mode 100644 index 0000000000000..57ee30f308f08 --- /dev/null +++ b/x-pack/plugins/security_solution/public/detection_engine/rule_management/model/prebuilt_rule_upgrade/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 './field_upgrade_state'; +export * from './fields_upgrade_state'; +export * from './rule_upgrade_state'; +export * from './rules_upgrade_state'; +export * from './set_rule_field_resolved_value'; diff --git a/x-pack/plugins/security_solution/public/detection_engine/rule_management/model/prebuilt_rule_upgrade/rule_upgrade_state.ts b/x-pack/plugins/security_solution/public/detection_engine/rule_management/model/prebuilt_rule_upgrade/rule_upgrade_state.ts new file mode 100644 index 0000000000000..0c72361bb29dc --- /dev/null +++ b/x-pack/plugins/security_solution/public/detection_engine/rule_management/model/prebuilt_rule_upgrade/rule_upgrade_state.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 DiffableRule, + type RuleUpgradeInfoForReview, +} from '../../../../../common/api/detection_engine'; +import type { FieldsUpgradeState } from './fields_upgrade_state'; + +export interface RuleUpgradeState extends RuleUpgradeInfoForReview { + /** + * Rule containing desired values users expect to see in the upgraded rule. + */ + finalRule: DiffableRule; + /** + * Indicates whether there are conflicts blocking rule upgrading. + */ + hasUnresolvedConflicts: boolean; + /** + * Stores a record of field names mapped to field upgrade state. + */ + fieldsUpgradeState: FieldsUpgradeState; +} diff --git a/x-pack/plugins/security_solution/public/detection_engine/rule_management/model/prebuilt_rule_upgrade/rules_upgrade_state.ts b/x-pack/plugins/security_solution/public/detection_engine/rule_management/model/prebuilt_rule_upgrade/rules_upgrade_state.ts new file mode 100644 index 0000000000000..66709ec34653e --- /dev/null +++ b/x-pack/plugins/security_solution/public/detection_engine/rule_management/model/prebuilt_rule_upgrade/rules_upgrade_state.ts @@ -0,0 +1,11 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import type { RuleSignatureId } from '../../../../../common/api/detection_engine'; +import type { RuleUpgradeState } from './rule_upgrade_state'; + +export type RulesUpgradeState = Record; diff --git a/x-pack/plugins/security_solution/public/detection_engine/rule_management/model/prebuilt_rule_upgrade/set_rule_field_resolved_value.ts b/x-pack/plugins/security_solution/public/detection_engine/rule_management/model/prebuilt_rule_upgrade/set_rule_field_resolved_value.ts new file mode 100644 index 0000000000000..c4bb65f162394 --- /dev/null +++ b/x-pack/plugins/security_solution/public/detection_engine/rule_management/model/prebuilt_rule_upgrade/set_rule_field_resolved_value.ts @@ -0,0 +1,16 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import type { DiffableAllFields, RuleObjectId } from '../../../../../common/api/detection_engine'; + +export type SetRuleFieldResolvedValueFn< + FieldName extends keyof DiffableAllFields = keyof DiffableAllFields +> = (params: { + ruleId: RuleObjectId; + fieldName: FieldName; + resolvedValue: DiffableAllFields[FieldName]; +}) => void; diff --git a/x-pack/plugins/security_solution/public/detection_engine/rule_management_ui/components/rules_table/bulk_actions/use_bulk_actions.tsx b/x-pack/plugins/security_solution/public/detection_engine/rule_management_ui/components/rules_table/bulk_actions/use_bulk_actions.tsx index c2c176563ca48..68e58b4db073f 100644 --- a/x-pack/plugins/security_solution/public/detection_engine/rule_management_ui/components/rules_table/bulk_actions/use_bulk_actions.tsx +++ b/x-pack/plugins/security_solution/public/detection_engine/rule_management_ui/components/rules_table/bulk_actions/use_bulk_actions.tsx @@ -16,7 +16,6 @@ import { MAX_MANUAL_RULE_RUN_BULK_SIZE } from '../../../../../../common/constant import type { TimeRange } from '../../../../rule_gaps/types'; import { useKibana } from '../../../../../common/lib/kibana'; import { convertRulesFilterToKQL } from '../../../../../../common/detection_engine/rule_management/rule_filtering'; -import { useIsExperimentalFeatureEnabled } from '../../../../../common/hooks/use_experimental_features'; import { DuplicateOptions } from '../../../../../../common/detection_engine/rule_management/constants'; import type { BulkActionEditPayload, @@ -89,7 +88,6 @@ export const useBulkActions = ({ actions: { clearRulesSelection, setIsPreflightInProgress }, } = rulesTableContext; - const isManualRuleRunEnabled = useIsExperimentalFeatureEnabled('manualRuleRunEnabled'); const getBulkItemsPopoverContent = useCallback( (closePopover: () => void): EuiContextMenuPanelDescriptor[] => { const selectedRules = rules.filter(({ id }) => selectedRuleIds.includes(id)); @@ -448,18 +446,14 @@ export const useBulkActions = ({ onClick: handleExportAction, icon: undefined, }, - ...(isManualRuleRunEnabled - ? [ - { - key: i18n.BULK_ACTION_MANUAL_RULE_RUN, - name: i18n.BULK_ACTION_MANUAL_RULE_RUN, - 'data-test-subj': 'scheduleRuleRunBulk', - disabled: containsLoading || (!containsEnabled && !isAllSelected), - onClick: handleScheduleRuleRunAction, - icon: undefined, - }, - ] - : []), + { + key: i18n.BULK_ACTION_MANUAL_RULE_RUN, + name: i18n.BULK_ACTION_MANUAL_RULE_RUN, + 'data-test-subj': 'scheduleRuleRunBulk', + disabled: containsLoading || (!containsEnabled && !isAllSelected), + onClick: handleScheduleRuleRunAction, + icon: undefined, + }, { key: i18n.BULK_ACTION_DISABLE, name: i18n.BULK_ACTION_DISABLE, @@ -600,7 +594,6 @@ export const useBulkActions = ({ filterOptions, completeBulkEditForm, startServices, - isManualRuleRunEnabled, ] ); diff --git a/x-pack/plugins/security_solution/public/detection_engine/rule_management_ui/components/rules_table/upgrade_prebuilt_rules_table/upgrade_prebuilt_rules_table.tsx b/x-pack/plugins/security_solution/public/detection_engine/rule_management_ui/components/rules_table/upgrade_prebuilt_rules_table/upgrade_prebuilt_rules_table.tsx index 16ba012313f34..2437a5e87866d 100644 --- a/x-pack/plugins/security_solution/public/detection_engine/rule_management_ui/components/rules_table/upgrade_prebuilt_rules_table/upgrade_prebuilt_rules_table.tsx +++ b/x-pack/plugins/security_solution/public/detection_engine/rule_management_ui/components/rules_table/upgrade_prebuilt_rules_table/upgrade_prebuilt_rules_table.tsx @@ -16,6 +16,7 @@ import { EuiSkeletonTitle, } from '@elastic/eui'; import React, { useMemo, useState } from 'react'; +import type { RuleUpgradeState } from '../../../../rule_management/model/prebuilt_rule_upgrade'; import * as i18n from '../../../../../detections/pages/detection_engine/rules/translations'; import { RULES_TABLE_INITIAL_PAGE_SIZE, RULES_TABLE_PAGE_SIZE_OPTIONS } from '../constants'; import { RulesChangelogLink } from '../rules_changelog_link'; @@ -23,7 +24,6 @@ import { UpgradePrebuiltRulesTableButtons } from './upgrade_prebuilt_rules_table import { useUpgradePrebuiltRulesTableContext } from './upgrade_prebuilt_rules_table_context'; import { UpgradePrebuiltRulesTableFilters } from './upgrade_prebuilt_rules_table_filters'; import { useUpgradePrebuiltRulesTableColumns } from './use_upgrade_prebuilt_rules_table_columns'; -import type { RuleUpgradeState } from './use_prebuilt_rules_upgrade_state'; const NO_ITEMS_MESSAGE = ( ; -export type SetRuleFieldResolvedValueFn< - FieldName extends keyof DiffableAllFields = keyof DiffableAllFields -> = (params: { - ruleId: RuleObjectId; - fieldName: FieldName; - resolvedValue: DiffableAllFields[FieldName]; -}) => void; - type RuleResolvedConflicts = Partial; type RulesResolvedConflicts = Record; @@ -70,6 +55,10 @@ export function usePrebuiltRulesUpgradeState( ruleUpgradeInfo, rulesResolvedConflicts[ruleUpgradeInfo.rule_id] ?? {} ), + fieldsUpgradeState: calcFieldsState( + ruleUpgradeInfo.diff.fields, + rulesResolvedConflicts[ruleUpgradeInfo.rule_id] ?? {} + ), hasUnresolvedConflicts: getUnacceptedConflictsCount( ruleUpgradeInfo.diff.fields, @@ -113,6 +102,35 @@ function convertRuleFieldsDiffToDiffable( return mergeVersionRule; } +function calcFieldsState( + ruleFieldsDiff: FieldsDiff>, + ruleResolvedConflicts: RuleResolvedConflicts +): FieldsUpgradeState { + const fieldsState: FieldsUpgradeState = {}; + + for (const fieldName of Object.keys(ruleFieldsDiff)) { + switch (ruleFieldsDiff[fieldName].conflict) { + case ThreeWayDiffConflict.NONE: + fieldsState[fieldName] = FieldUpgradeState.Accepted; + break; + + case ThreeWayDiffConflict.SOLVABLE: + fieldsState[fieldName] = FieldUpgradeState.SolvableConflict; + break; + + case ThreeWayDiffConflict.NON_SOLVABLE: + fieldsState[fieldName] = FieldUpgradeState.NonSolvableConflict; + break; + } + } + + for (const fieldName of Object.keys(ruleResolvedConflicts)) { + fieldsState[fieldName] = FieldUpgradeState.Accepted; + } + + return fieldsState; +} + function getUnacceptedConflictsCount( ruleFieldsDiff: FieldsDiff>, ruleResolvedConflicts: RuleResolvedConflicts diff --git a/x-pack/plugins/security_solution/public/detection_engine/rule_management_ui/components/rules_table/upgrade_prebuilt_rules_table/use_upgrade_prebuilt_rules_table_columns.tsx b/x-pack/plugins/security_solution/public/detection_engine/rule_management_ui/components/rules_table/upgrade_prebuilt_rules_table/use_upgrade_prebuilt_rules_table_columns.tsx index e7267007d2348..09009c98c2858 100644 --- a/x-pack/plugins/security_solution/public/detection_engine/rule_management_ui/components/rules_table/upgrade_prebuilt_rules_table/use_upgrade_prebuilt_rules_table_columns.tsx +++ b/x-pack/plugins/security_solution/public/detection_engine/rule_management_ui/components/rules_table/upgrade_prebuilt_rules_table/use_upgrade_prebuilt_rules_table_columns.tsx @@ -8,6 +8,7 @@ import type { EuiBasicTableColumn } from '@elastic/eui'; import { EuiBadge, EuiButtonEmpty, EuiLink, EuiLoadingSpinner, EuiText } from '@elastic/eui'; import React, { useMemo } from 'react'; +import type { RuleUpgradeState } from '../../../../rule_management/model/prebuilt_rule_upgrade/rule_upgrade_state'; import { RulesTableEmptyColumnName } from '../rules_table_empty_column_name'; import { SHOW_RELATED_INTEGRATIONS_SETTING } from '../../../../../../common/constants'; import type { RuleSignatureId } from '../../../../../../common/api/detection_engine/model/rule_schema'; @@ -22,7 +23,6 @@ import type { Rule } from '../../../../rule_management/logic'; import { getNormalizedSeverity } from '../helpers'; import type { UpgradePrebuiltRulesTableActions } from './upgrade_prebuilt_rules_table_context'; import { useUpgradePrebuiltRulesTableContext } from './upgrade_prebuilt_rules_table_context'; -import type { RuleUpgradeState } from './use_prebuilt_rules_upgrade_state'; export type TableColumn = EuiBasicTableColumn; diff --git a/x-pack/plugins/security_solution/public/detection_engine/rule_management_ui/components/rules_table/use_rules_table_actions.tsx b/x-pack/plugins/security_solution/public/detection_engine/rule_management_ui/components/rules_table/use_rules_table_actions.tsx index 984df06342a1a..4cc7a03426657 100644 --- a/x-pack/plugins/security_solution/public/detection_engine/rule_management_ui/components/rules_table/use_rules_table_actions.tsx +++ b/x-pack/plugins/security_solution/public/detection_engine/rule_management_ui/components/rules_table/use_rules_table_actions.tsx @@ -8,7 +8,6 @@ import type { DefaultItemAction } from '@elastic/eui'; import { EuiToolTip } from '@elastic/eui'; import React from 'react'; -import { useIsExperimentalFeatureEnabled } from '../../../../common/hooks/use_experimental_features'; import { DuplicateOptions } from '../../../../../common/detection_engine/rule_management/constants'; import { BulkActionTypeEnum } from '../../../../../common/api/detection_engine/rule_management'; import { SINGLE_RULE_ACTIONS } from '../../../../common/lib/apm/user_actions'; @@ -47,8 +46,6 @@ export const useRulesTableActions = ({ const downloadExportedRules = useDownloadExportedRules(); const { scheduleRuleRun } = useScheduleRuleRun(); - const isManualRuleRunEnabled = useIsExperimentalFeatureEnabled('manualRuleRunEnabled'); - return [ { type: 'icon', @@ -120,33 +117,28 @@ export const useRulesTableActions = ({ }, enabled: (rule: Rule) => !rule.immutable, }, - ...(isManualRuleRunEnabled - ? [ - { - type: 'icon', - 'data-test-subj': 'manualRuleRunAction', - description: (rule) => - !rule.enabled ? i18n.MANUAL_RULE_RUN_TOOLTIP : i18n.MANUAL_RULE_RUN, - icon: 'play', - name: i18n.MANUAL_RULE_RUN, - onClick: async (rule: Rule) => { - startTransaction({ name: SINGLE_RULE_ACTIONS.MANUAL_RULE_RUN }); - const modalManualRuleRunConfirmationResult = await showManualRuleRunConfirmation(); - telemetry.reportManualRuleRunOpenModal({ - type: 'single', - }); - if (modalManualRuleRunConfirmationResult === null) { - return; - } - await scheduleRuleRun({ - ruleIds: [rule.id], - timeRange: modalManualRuleRunConfirmationResult, - }); - }, - enabled: (rule: Rule) => rule.enabled, - } as DefaultItemAction, - ] - : []), + { + type: 'icon', + 'data-test-subj': 'manualRuleRunAction', + description: (rule) => (!rule.enabled ? i18n.MANUAL_RULE_RUN_TOOLTIP : i18n.MANUAL_RULE_RUN), + icon: 'play', + name: i18n.MANUAL_RULE_RUN, + onClick: async (rule: Rule) => { + startTransaction({ name: SINGLE_RULE_ACTIONS.MANUAL_RULE_RUN }); + const modalManualRuleRunConfirmationResult = await showManualRuleRunConfirmation(); + telemetry.reportManualRuleRunOpenModal({ + type: 'single', + }); + if (modalManualRuleRunConfirmationResult === null) { + return; + } + await scheduleRuleRun({ + ruleIds: [rule.id], + timeRange: modalManualRuleRunConfirmationResult, + }); + }, + enabled: (rule: Rule) => rule.enabled, + }, { type: 'icon', 'data-test-subj': 'deleteRuleAction', diff --git a/x-pack/plugins/security_solution/public/detection_engine/rule_monitoring/components/execution_results_table/use_execution_results.tsx b/x-pack/plugins/security_solution/public/detection_engine/rule_monitoring/components/execution_results_table/use_execution_results.tsx index 8660139676351..e6ee5769ee822 100644 --- a/x-pack/plugins/security_solution/public/detection_engine/rule_monitoring/components/execution_results_table/use_execution_results.tsx +++ b/x-pack/plugins/security_solution/public/detection_engine/rule_monitoring/components/execution_results_table/use_execution_results.tsx @@ -7,29 +7,20 @@ import { useQuery } from '@tanstack/react-query'; import { useAppToasts } from '../../../../common/hooks/use_app_toasts'; - -import { RuleRunTypeEnum } from '../../../../../common/api/detection_engine/rule_monitoring'; import type { GetRuleExecutionResultsResponse } from '../../../../../common/api/detection_engine/rule_monitoring'; import type { FetchRuleExecutionResultsArgs } from '../../api'; import { api } from '../../api'; -import { useIsExperimentalFeatureEnabled } from '../../../../common/hooks/use_experimental_features'; import * as i18n from './translations'; export type UseExecutionResultsArgs = Omit; export const useExecutionResults = (args: UseExecutionResultsArgs) => { const { addError } = useAppToasts(); - const isManualRuleRunEnabled = useIsExperimentalFeatureEnabled('manualRuleRunEnabled'); return useQuery( ['detectionEngine', 'ruleMonitoring', 'executionResults', args], ({ signal }) => { - let runTypeFilters = args.runTypeFilters; - - // if manual rule run is disabled, only show standard runs - if (!isManualRuleRunEnabled) { - runTypeFilters = [RuleRunTypeEnum.standard]; - } + const runTypeFilters = args.runTypeFilters; return api.fetchRuleExecutionResults({ ...args, runTypeFilters, signal }); }, diff --git a/x-pack/plugins/security_solution/public/detections/components/rules/rule_actions_overflow/index.test.tsx b/x-pack/plugins/security_solution/public/detections/components/rules/rule_actions_overflow/index.test.tsx index 298ae1c503533..e1ff950bc5e32 100644 --- a/x-pack/plugins/security_solution/public/detections/components/rules/rule_actions_overflow/index.test.tsx +++ b/x-pack/plugins/security_solution/public/detections/components/rules/rule_actions_overflow/index.test.tsx @@ -274,25 +274,6 @@ describe('RuleActionsOverflow', () => { expect(getByTestId('rules-details-popover')).not.toHaveTextContent(/.+/); }); - test('it does not show "Manual run" action item when feature flag "manualRuleRunEnabled" is set to false', () => { - useIsExperimentalFeatureEnabledMock.mockReturnValue(false); - - const { getByTestId } = render( - Promise.resolve(true)} - />, - { wrapper: TestProviders } - ); - fireEvent.click(getByTestId('rules-details-popover-button-icon')); - - expect(getByTestId('rules-details-menu-panel')).not.toHaveTextContent('Manual run'); - }); - test('it calls telemetry.reportManualRuleRunOpenModal when rules-details-manual-rule-run is clicked', async () => { const { getByTestId } = render( { navigateToApp(APP_UI_ID, { deepLinkId: SecurityPageName.rules, @@ -152,39 +149,32 @@ const RuleActionsOverflowComponent = ({ > {i18nActions.EXPORT_RULE} , - ...(isManualRuleRunEnabled - ? [ - { - startTransaction({ name: SINGLE_RULE_ACTIONS.MANUAL_RULE_RUN }); - closePopover(); - const modalManualRuleRunConfirmationResult = - await showManualRuleRunConfirmation(); - telemetry.reportManualRuleRunOpenModal({ - type: 'single', - }); - if (modalManualRuleRunConfirmationResult === null) { - return; - } - await scheduleRuleRun({ - ruleIds: [rule.id], - timeRange: modalManualRuleRunConfirmationResult, - }); - }} - > - {i18nActions.MANUAL_RULE_RUN} - , - ] - : []), + { + startTransaction({ name: SINGLE_RULE_ACTIONS.MANUAL_RULE_RUN }); + closePopover(); + const modalManualRuleRunConfirmationResult = await showManualRuleRunConfirmation(); + telemetry.reportManualRuleRunOpenModal({ + type: 'single', + }); + if (modalManualRuleRunConfirmationResult === null) { + return; + } + await scheduleRuleRun({ + ruleIds: [rule.id], + timeRange: modalManualRuleRunConfirmationResult, + }); + }} + > + {i18nActions.MANUAL_RULE_RUN} + , { - uiMetricService.trackUiMetric(METRIC_TYPE.COUNT, ENTITY_FLYOUT_MISCONFIGURATION_VIEW_VISITS); - return { id: EntityDetailsLeftPanelTab.CSP_INSIGHTS, 'data-test-subj': INSIGHTS_TAB_TEST_ID, diff --git a/x-pack/plugins/security_solution/public/flyout/document_details/left/components/host_details.test.tsx b/x-pack/plugins/security_solution/public/flyout/document_details/left/components/host_details.test.tsx index 46288434f48bb..23f6969c36778 100644 --- a/x-pack/plugins/security_solution/public/flyout/document_details/left/components/host_details.test.tsx +++ b/x-pack/plugins/security_solution/public/flyout/document_details/left/components/host_details.test.tsx @@ -7,6 +7,8 @@ import React from 'react'; import { render } from '@testing-library/react'; +import { useMisconfigurationPreview } from '@kbn/cloud-security-posture/src/hooks/use_misconfiguration_preview'; +import { useVulnerabilitiesPreview } from '@kbn/cloud-security-posture/src/hooks/use_vulnerabilities_preview'; import type { Anomalies } from '../../../../common/components/ml/types'; import { DocumentDetailsContext } from '../../shared/context'; import { TestProviders } from '../../../../common/mock'; @@ -24,6 +26,9 @@ import { HOST_DETAILS_LINK_TEST_ID, HOST_DETAILS_RELATED_USERS_LINK_TEST_ID, HOST_DETAILS_RELATED_USERS_IP_LINK_TEST_ID, + HOST_DETAILS_MISCONFIGURATIONS_TEST_ID, + HOST_DETAILS_VULNERABILITIES_TEST_ID, + HOST_DETAILS_ALERT_COUNT_TEST_ID, } from './test_ids'; import { EXPANDABLE_PANEL_CONTENT_TEST_ID } from '@kbn/security-solution-common'; import { useRiskScore } from '../../../../entity_analytics/api/hooks/use_risk_score'; @@ -35,8 +40,11 @@ import { HOST_PREVIEW_BANNER } from '../../right/components/host_entity_overview import { UserPreviewPanelKey } from '../../../entity_details/user_right'; import { USER_PREVIEW_BANNER } from '../../right/components/user_entity_overview'; import { NetworkPanelKey, NETWORK_PREVIEW_BANNER } from '../../../network_details'; +import { useSummaryChartData } from '../../../../detections/components/alerts_kpis/alerts_summary_charts_panel/use_summary_chart_data'; jest.mock('@kbn/expandable-flyout'); +jest.mock('@kbn/cloud-security-posture/src/hooks/use_misconfiguration_preview'); +jest.mock('@kbn/cloud-security-posture/src/hooks/use_vulnerabilities_preview'); jest.mock('../../../../common/hooks/use_experimental_features'); const mockUseIsExperimentalFeatureEnabled = useIsExperimentalFeatureEnabled as jest.Mock; @@ -104,6 +112,10 @@ const mockUseHostsRelatedUsers = useHostRelatedUsers as jest.Mock; jest.mock('../../../../entity_analytics/api/hooks/use_risk_score'); const mockUseRiskScore = useRiskScore as jest.Mock; +jest.mock( + '../../../../detections/components/alerts_kpis/alerts_summary_charts_panel/use_summary_chart_data' +); + const timestamp = '2022-07-25T08:20:18.966Z'; const defaultProps = { @@ -158,6 +170,9 @@ describe('', () => { mockUseRiskScore.mockReturnValue(mockRiskScoreResponse); mockUseHostsRelatedUsers.mockReturnValue(mockRelatedUsersResponse); mockUseIsExperimentalFeatureEnabled.mockReturnValue(true); + (useMisconfigurationPreview as jest.Mock).mockReturnValue({}); + (useVulnerabilitiesPreview as jest.Mock).mockReturnValue({}); + (useSummaryChartData as jest.Mock).mockReturnValue({ isLoading: false, items: [] }); }); it('should render host details correctly', () => { @@ -296,4 +311,41 @@ describe('', () => { }); }); }); + + describe('distribution bar insights', () => { + it('should not render if no data is available', () => { + const { queryByTestId } = renderHostDetails(mockContextValue); + expect(queryByTestId(HOST_DETAILS_MISCONFIGURATIONS_TEST_ID)).not.toBeInTheDocument(); + expect(queryByTestId(HOST_DETAILS_VULNERABILITIES_TEST_ID)).not.toBeInTheDocument(); + expect(queryByTestId(HOST_DETAILS_ALERT_COUNT_TEST_ID)).not.toBeInTheDocument(); + }); + + it('should render alert count when data is available', () => { + (useSummaryChartData as jest.Mock).mockReturnValue({ + isLoading: false, + items: [{ key: 'high', value: 78, label: 'High' }], + }); + + const { getByTestId } = renderHostDetails(mockContextValue); + expect(getByTestId(HOST_DETAILS_ALERT_COUNT_TEST_ID)).toBeInTheDocument(); + }); + + it('should render misconfiguration when data is available', () => { + (useMisconfigurationPreview as jest.Mock).mockReturnValue({ + data: { count: { passed: 1, failed: 2 } }, + }); + + const { getByTestId } = renderHostDetails(mockContextValue); + expect(getByTestId(HOST_DETAILS_MISCONFIGURATIONS_TEST_ID)).toBeInTheDocument(); + }); + + it('should render vulnerabilities when data is available', () => { + (useVulnerabilitiesPreview as jest.Mock).mockReturnValue({ + data: { count: { CRITICAL: 0, HIGH: 1, MEDIUM: 1, LOW: 0, UNKNOWN: 0 } }, + }); + + const { getByTestId } = renderHostDetails(mockContextValue); + expect(getByTestId(HOST_DETAILS_VULNERABILITIES_TEST_ID)).toBeInTheDocument(); + }); + }); }); diff --git a/x-pack/plugins/security_solution/public/flyout/document_details/left/components/host_details.tsx b/x-pack/plugins/security_solution/public/flyout/document_details/left/components/host_details.tsx index 33b8bb22fce53..122caa657b039 100644 --- a/x-pack/plugins/security_solution/public/flyout/document_details/left/components/host_details.tsx +++ b/x-pack/plugins/security_solution/public/flyout/document_details/left/components/host_details.tsx @@ -18,6 +18,8 @@ import { EuiToolTip, EuiIcon, EuiPanel, + EuiHorizontalRule, + EuiFlexGrid, } from '@elastic/eui'; import type { EuiBasicTableColumn } from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n-react'; @@ -51,6 +53,9 @@ import { HOST_DETAILS_RELATED_USERS_TABLE_TEST_ID, HOST_DETAILS_RELATED_USERS_LINK_TEST_ID, HOST_DETAILS_RELATED_USERS_IP_LINK_TEST_ID, + HOST_DETAILS_ALERT_COUNT_TEST_ID, + HOST_DETAILS_MISCONFIGURATIONS_TEST_ID, + HOST_DETAILS_VULNERABILITIES_TEST_ID, } from './test_ids'; import { USER_NAME_FIELD_NAME, @@ -63,6 +68,9 @@ import { PreviewLink } from '../../../shared/components/preview_link'; import { HostPreviewPanelKey } from '../../../entity_details/host_right'; import { HOST_PREVIEW_BANNER } from '../../right/components/host_entity_overview'; import type { NarrowDateRange } from '../../../../common/components/ml/types'; +import { MisconfigurationsInsight } from '../../shared/components/misconfiguration_insight'; +import { VulnerabilitiesInsight } from '../../shared/components/vulnerabilities_insight'; +import { AlertCountInsight } from '../../shared/components/alert_count_insight'; const HOST_DETAILS_ID = 'entities-hosts-details'; const RELATED_USERS_ID = 'entities-hosts-related-users'; @@ -337,6 +345,28 @@ export const HostDetails: React.FC = ({ hostName, timestamp, s )} + + + + + + + + diff --git a/x-pack/plugins/security_solution/public/flyout/document_details/left/components/test_ids.ts b/x-pack/plugins/security_solution/public/flyout/document_details/left/components/test_ids.ts index 0779f3c135b2d..8669b504f6861 100644 --- a/x-pack/plugins/security_solution/public/flyout/document_details/left/components/test_ids.ts +++ b/x-pack/plugins/security_solution/public/flyout/document_details/left/components/test_ids.ts @@ -43,6 +43,9 @@ export const PREVALENCE_DETAILS_TABLE_UPSELL_CELL_TEST_ID = export const ENTITIES_DETAILS_TEST_ID = `${PREFIX}EntitiesDetails` as const; export const USER_DETAILS_TEST_ID = `${PREFIX}UsersDetails` as const; export const USER_DETAILS_LINK_TEST_ID = `${USER_DETAILS_TEST_ID}TitleLink` as const; +export const USER_DETAILS_ALERT_COUNT_TEST_ID = `${USER_DETAILS_TEST_ID}AlertCount` as const; +export const USER_DETAILS_MISCONFIGURATIONS_TEST_ID = + `${USER_DETAILS_TEST_ID}Misconfigurations` as const; export const USER_DETAILS_RELATED_HOSTS_TABLE_TEST_ID = `${USER_DETAILS_TEST_ID}RelatedHostsTable` as const; export const USER_DETAILS_RELATED_HOSTS_LINK_TEST_ID = @@ -53,6 +56,11 @@ export const USER_DETAILS_INFO_TEST_ID = 'user-overview' as const; export const HOST_DETAILS_TEST_ID = `${PREFIX}HostsDetails` as const; export const HOST_DETAILS_LINK_TEST_ID = `${HOST_DETAILS_TEST_ID}TitleLink` as const; +export const HOST_DETAILS_ALERT_COUNT_TEST_ID = `${HOST_DETAILS_TEST_ID}AlertCount` as const; +export const HOST_DETAILS_MISCONFIGURATIONS_TEST_ID = + `${HOST_DETAILS_TEST_ID}Misconfigurations` as const; +export const HOST_DETAILS_VULNERABILITIES_TEST_ID = + `${HOST_DETAILS_TEST_ID}Vulnerabilities` as const; export const HOST_DETAILS_RELATED_USERS_TABLE_TEST_ID = `${HOST_DETAILS_TEST_ID}RelatedUsersTable` as const; export const HOST_DETAILS_RELATED_USERS_LINK_TEST_ID = diff --git a/x-pack/plugins/security_solution/public/flyout/document_details/left/components/user_details.test.tsx b/x-pack/plugins/security_solution/public/flyout/document_details/left/components/user_details.test.tsx index c1ed881e80a95..a2c53afb8c3f3 100644 --- a/x-pack/plugins/security_solution/public/flyout/document_details/left/components/user_details.test.tsx +++ b/x-pack/plugins/security_solution/public/flyout/document_details/left/components/user_details.test.tsx @@ -7,6 +7,7 @@ import React from 'react'; import { render } from '@testing-library/react'; +import { useMisconfigurationPreview } from '@kbn/cloud-security-posture/src/hooks/use_misconfiguration_preview'; import type { Anomalies } from '../../../../common/components/ml/types'; import { TestProviders } from '../../../../common/mock'; import { DocumentDetailsContext } from '../../shared/context'; @@ -24,6 +25,8 @@ import { USER_DETAILS_RELATED_HOSTS_TABLE_TEST_ID, USER_DETAILS_RELATED_HOSTS_LINK_TEST_ID, USER_DETAILS_RELATED_HOSTS_IP_LINK_TEST_ID, + USER_DETAILS_MISCONFIGURATIONS_TEST_ID, + USER_DETAILS_ALERT_COUNT_TEST_ID, } from './test_ids'; import { EXPANDABLE_PANEL_CONTENT_TEST_ID } from '@kbn/security-solution-common'; import { useRiskScore } from '../../../../entity_analytics/api/hooks/use_risk_score'; @@ -35,8 +38,10 @@ import { HOST_PREVIEW_BANNER } from '../../right/components/host_entity_overview import { UserPreviewPanelKey } from '../../../entity_details/user_right'; import { USER_PREVIEW_BANNER } from '../../right/components/user_entity_overview'; import { NetworkPanelKey, NETWORK_PREVIEW_BANNER } from '../../../network_details'; +import { useSummaryChartData } from '../../../../detections/components/alerts_kpis/alerts_summary_charts_panel/use_summary_chart_data'; jest.mock('@kbn/expandable-flyout'); +jest.mock('@kbn/cloud-security-posture/src/hooks/use_misconfiguration_preview'); jest.mock('../../../../common/hooks/use_experimental_features'); const mockUseIsExperimentalFeatureEnabled = useIsExperimentalFeatureEnabled as jest.Mock; @@ -101,6 +106,10 @@ const mockUseUsersRelatedHosts = useUserRelatedHosts as jest.Mock; jest.mock('../../../../entity_analytics/api/hooks/use_risk_score'); const mockUseRiskScore = useRiskScore as jest.Mock; +jest.mock( + '../../../../detections/components/alerts_kpis/alerts_summary_charts_panel/use_summary_chart_data' +); + const timestamp = '2022-07-25T08:20:18.966Z'; const defaultProps = { @@ -155,6 +164,8 @@ describe('', () => { mockUseRiskScore.mockReturnValue(mockRiskScoreResponse); mockUseUsersRelatedHosts.mockReturnValue(mockRelatedHostsResponse); mockUseIsExperimentalFeatureEnabled.mockReturnValue(true); + (useMisconfigurationPreview as jest.Mock).mockReturnValue({}); + (useSummaryChartData as jest.Mock).mockReturnValue({ isLoading: false, items: [] }); }); it('should render user details correctly', () => { @@ -278,4 +289,31 @@ describe('', () => { }); }); }); + + describe('distribution bar insights', () => { + it('should not render if no data is available', () => { + const { queryByTestId } = renderUserDetails(mockContextValue); + expect(queryByTestId(USER_DETAILS_MISCONFIGURATIONS_TEST_ID)).not.toBeInTheDocument(); + expect(queryByTestId(USER_DETAILS_ALERT_COUNT_TEST_ID)).not.toBeInTheDocument(); + }); + + it('should render alert count when data is available', () => { + (useSummaryChartData as jest.Mock).mockReturnValue({ + isLoading: false, + items: [{ key: 'high', value: 78, label: 'High' }], + }); + + const { getByTestId } = renderUserDetails(mockContextValue); + expect(getByTestId(USER_DETAILS_ALERT_COUNT_TEST_ID)).toBeInTheDocument(); + }); + + it('should render misconfiguration when data is available', () => { + (useMisconfigurationPreview as jest.Mock).mockReturnValue({ + data: { count: { passed: 1, failed: 2 } }, + }); + + const { getByTestId } = renderUserDetails(mockContextValue); + expect(getByTestId(USER_DETAILS_MISCONFIGURATIONS_TEST_ID)).toBeInTheDocument(); + }); + }); }); diff --git a/x-pack/plugins/security_solution/public/flyout/document_details/left/components/user_details.tsx b/x-pack/plugins/security_solution/public/flyout/document_details/left/components/user_details.tsx index 13d3e825053ba..c90d11f4b8bc2 100644 --- a/x-pack/plugins/security_solution/public/flyout/document_details/left/components/user_details.tsx +++ b/x-pack/plugins/security_solution/public/flyout/document_details/left/components/user_details.tsx @@ -18,6 +18,8 @@ import { EuiFlexItem, EuiToolTip, EuiPanel, + EuiHorizontalRule, + EuiFlexGrid, } from '@elastic/eui'; import type { EuiBasicTableColumn } from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n-react'; @@ -51,6 +53,8 @@ import { USER_DETAILS_TEST_ID, USER_DETAILS_RELATED_HOSTS_LINK_TEST_ID, USER_DETAILS_RELATED_HOSTS_IP_LINK_TEST_ID, + USER_DETAILS_MISCONFIGURATIONS_TEST_ID, + USER_DETAILS_ALERT_COUNT_TEST_ID, } from './test_ids'; import { HOST_NAME_FIELD_NAME, @@ -63,6 +67,8 @@ import { UserPreviewPanelKey } from '../../../entity_details/user_right'; import { USER_PREVIEW_BANNER } from '../../right/components/user_entity_overview'; import { PreviewLink } from '../../../shared/components/preview_link'; import type { NarrowDateRange } from '../../../../common/components/ml/types'; +import { MisconfigurationsInsight } from '../../shared/components/misconfiguration_insight'; +import { AlertCountInsight } from '../../shared/components/alert_count_insight'; const USER_DETAILS_ID = 'entities-users-details'; const RELATED_HOSTS_ID = 'entities-users-related-hosts'; @@ -340,6 +346,22 @@ export const UserDetails: React.FC = ({ userName, timestamp, s )} + + + + + + diff --git a/x-pack/plugins/security_solution/public/flyout/document_details/right/components/host_entity_overview.test.tsx b/x-pack/plugins/security_solution/public/flyout/document_details/right/components/host_entity_overview.test.tsx index b710df84e1a13..6ad90adb28997 100644 --- a/x-pack/plugins/security_solution/public/flyout/document_details/right/components/host_entity_overview.test.tsx +++ b/x-pack/plugins/security_solution/public/flyout/document_details/right/components/host_entity_overview.test.tsx @@ -6,6 +6,8 @@ */ import React from 'react'; import { render } from '@testing-library/react'; +import { useMisconfigurationPreview } from '@kbn/cloud-security-posture/src/hooks/use_misconfiguration_preview'; +import { useVulnerabilitiesPreview } from '@kbn/cloud-security-posture/src/hooks/use_vulnerabilities_preview'; import { TestProviders } from '../../../../common/mock'; import { HostEntityOverview, HOST_PREVIEW_BANNER } from './host_entity_overview'; import { useHostDetails } from '../../../../explore/hosts/containers/hosts/details'; @@ -16,6 +18,9 @@ import { ENTITIES_HOST_OVERVIEW_LINK_TEST_ID, ENTITIES_HOST_OVERVIEW_RISK_LEVEL_TEST_ID, ENTITIES_HOST_OVERVIEW_LOADING_TEST_ID, + ENTITIES_HOST_OVERVIEW_MISCONFIGURATIONS_TEST_ID, + ENTITIES_HOST_OVERVIEW_VULNERABILITIES_TEST_ID, + ENTITIES_HOST_OVERVIEW_ALERT_COUNT_TEST_ID, } from './test_ids'; import { DocumentDetailsContext } from '../../shared/context'; import { mockContextValue } from '../../shared/mocks/mock_context'; @@ -29,6 +34,7 @@ import { ENTITIES_TAB_ID } from '../../left/components/entities_details'; import { useRiskScore } from '../../../../entity_analytics/api/hooks/use_risk_score'; import { mockFlyoutApi } from '../../shared/mocks/mock_flyout_context'; import { createTelemetryServiceMock } from '../../../../common/lib/telemetry/telemetry_service.mock'; +import { useSummaryChartData } from '../../../../detections/components/alerts_kpis/alerts_summary_charts_panel/use_summary_chart_data'; const hostName = 'host'; const osFamily = 'Windows'; @@ -46,6 +52,17 @@ const panelContextValue = { }; jest.mock('@kbn/expandable-flyout'); +jest.mock('@kbn/cloud-security-posture/src/hooks/use_misconfiguration_preview'); +jest.mock('@kbn/cloud-security-posture/src/hooks/use_vulnerabilities_preview'); + +jest.mock('react-router-dom', () => { + const actual = jest.requireActual('react-router-dom'); + return { ...actual, useLocation: jest.fn().mockReturnValue({ pathname: '' }) }; +}); + +jest.mock( + '../../../../detections/components/alerts_kpis/alerts_summary_charts_panel/use_summary_chart_data' +); const mockedTelemetry = createTelemetryServiceMock(); jest.mock('../../../../common/lib/kibana', () => { @@ -99,6 +116,9 @@ describe('', () => { beforeAll(() => { jest.mocked(useExpandableFlyoutApi).mockReturnValue(mockFlyoutApi); mockUseIsExperimentalFeatureEnabled.mockReturnValue(true); + (useMisconfigurationPreview as jest.Mock).mockReturnValue({}); + (useVulnerabilitiesPreview as jest.Mock).mockReturnValue({}); + (useSummaryChartData as jest.Mock).mockReturnValue({ isLoading: false, items: [] }); }); describe('license is valid', () => { @@ -150,6 +170,7 @@ describe('', () => { ); expect(getByTestId(ENTITIES_HOST_OVERVIEW_LOADING_TEST_ID)).toBeInTheDocument(); }); + describe('license is not valid', () => { it('should render os family and last seen', () => { mockUseHostDetails.mockReturnValue([false, { hostDetails: hostData }]); @@ -210,4 +231,48 @@ describe('', () => { }); }); }); + + describe('distribution bar insights', () => { + beforeEach(() => { + mockUseHostDetails.mockReturnValue([false, { hostDetails: hostData }]); + mockUseRiskScore.mockReturnValue({ data: riskLevel, isAuthorized: true }); + }); + + it('should not render if no data is available', () => { + const { queryByTestId } = renderHostEntityContent(); + expect( + queryByTestId(ENTITIES_HOST_OVERVIEW_MISCONFIGURATIONS_TEST_ID) + ).not.toBeInTheDocument(); + expect(queryByTestId(ENTITIES_HOST_OVERVIEW_VULNERABILITIES_TEST_ID)).not.toBeInTheDocument(); + expect(queryByTestId(ENTITIES_HOST_OVERVIEW_ALERT_COUNT_TEST_ID)).not.toBeInTheDocument(); + }); + + it('should render alert count when data is available', () => { + (useSummaryChartData as jest.Mock).mockReturnValue({ + isLoading: false, + items: [{ key: 'high', value: 78, label: 'High' }], + }); + + const { getByTestId } = renderHostEntityContent(); + expect(getByTestId(ENTITIES_HOST_OVERVIEW_ALERT_COUNT_TEST_ID)).toBeInTheDocument(); + }); + + it('should render misconfiguration when data is available', () => { + (useMisconfigurationPreview as jest.Mock).mockReturnValue({ + data: { count: { passed: 1, failed: 2 } }, + }); + + const { getByTestId } = renderHostEntityContent(); + expect(getByTestId(ENTITIES_HOST_OVERVIEW_MISCONFIGURATIONS_TEST_ID)).toBeInTheDocument(); + }); + + it('should render vulnerabilities when data is available', () => { + (useVulnerabilitiesPreview as jest.Mock).mockReturnValue({ + data: { count: { CRITICAL: 0, HIGH: 1, MEDIUM: 1, LOW: 0, UNKNOWN: 0 } }, + }); + + const { getByTestId } = renderHostEntityContent(); + expect(getByTestId(ENTITIES_HOST_OVERVIEW_VULNERABILITIES_TEST_ID)).toBeInTheDocument(); + }); + }); }); diff --git a/x-pack/plugins/security_solution/public/flyout/document_details/right/components/host_entity_overview.tsx b/x-pack/plugins/security_solution/public/flyout/document_details/right/components/host_entity_overview.tsx index ca6a68eb23be8..90405286b004c 100644 --- a/x-pack/plugins/security_solution/public/flyout/document_details/right/components/host_entity_overview.tsx +++ b/x-pack/plugins/security_solution/public/flyout/document_details/right/components/host_entity_overview.tsx @@ -52,11 +52,17 @@ import { ENTITIES_HOST_OVERVIEW_RISK_LEVEL_TEST_ID, ENTITIES_HOST_OVERVIEW_LINK_TEST_ID, ENTITIES_HOST_OVERVIEW_LOADING_TEST_ID, + ENTITIES_HOST_OVERVIEW_ALERT_COUNT_TEST_ID, + ENTITIES_HOST_OVERVIEW_MISCONFIGURATIONS_TEST_ID, + ENTITIES_HOST_OVERVIEW_VULNERABILITIES_TEST_ID, } from './test_ids'; import { DocumentDetailsLeftPanelKey } from '../../shared/constants/panel_keys'; import { LeftPanelInsightsTab } from '../../left'; import { RiskScoreDocTooltip } from '../../../../overview/components/common'; import { PreviewLink } from '../../../shared/components/preview_link'; +import { MisconfigurationsInsight } from '../../shared/components/misconfiguration_insight'; +import { VulnerabilitiesInsight } from '../../shared/components/vulnerabilities_insight'; +import { AlertCountInsight } from '../../shared/components/alert_count_insight'; const HOST_ICON = 'storage'; @@ -196,12 +202,12 @@ export const HostEntityOverview: React.FC = ({ hostName return ( - + @@ -270,6 +276,20 @@ export const HostEntityOverview: React.FC = ({ hostName )} + + + ); }; diff --git a/x-pack/plugins/security_solution/public/flyout/document_details/right/components/test_ids.ts b/x-pack/plugins/security_solution/public/flyout/document_details/right/components/test_ids.ts index 40670ddc7110a..e0d8bc6db0f5c 100644 --- a/x-pack/plugins/security_solution/public/flyout/document_details/right/components/test_ids.ts +++ b/x-pack/plugins/security_solution/public/flyout/document_details/right/components/test_ids.ts @@ -121,6 +121,10 @@ export const ENTITIES_USER_OVERVIEW_LAST_SEEN_TEST_ID = `${ENTITIES_USER_OVERVIEW_TEST_ID}LastSeen` as const; export const ENTITIES_USER_OVERVIEW_RISK_LEVEL_TEST_ID = `${ENTITIES_USER_OVERVIEW_TEST_ID}RiskLevel` as const; +export const ENTITIES_USER_OVERVIEW_ALERT_COUNT_TEST_ID = + `${ENTITIES_USER_OVERVIEW_TEST_ID}AlertCount` as const; +export const ENTITIES_USER_OVERVIEW_MISCONFIGURATIONS_TEST_ID = + `${ENTITIES_USER_OVERVIEW_TEST_ID}Misconfigurations` as const; export const ENTITIES_HOST_OVERVIEW_TEST_ID = `${INSIGHTS_ENTITIES_TEST_ID}HostOverview` as const; export const ENTITIES_HOST_OVERVIEW_LOADING_TEST_ID = @@ -132,6 +136,12 @@ export const ENTITIES_HOST_OVERVIEW_LAST_SEEN_TEST_ID = `${ENTITIES_HOST_OVERVIEW_TEST_ID}LastSeen` as const; export const ENTITIES_HOST_OVERVIEW_RISK_LEVEL_TEST_ID = `${ENTITIES_HOST_OVERVIEW_TEST_ID}RiskLevel` as const; +export const ENTITIES_HOST_OVERVIEW_ALERT_COUNT_TEST_ID = + `${ENTITIES_HOST_OVERVIEW_TEST_ID}AlertCount` as const; +export const ENTITIES_HOST_OVERVIEW_MISCONFIGURATIONS_TEST_ID = + `${ENTITIES_HOST_OVERVIEW_TEST_ID}Misconfigurations` as const; +export const ENTITIES_HOST_OVERVIEW_VULNERABILITIES_TEST_ID = + `${ENTITIES_HOST_OVERVIEW_TEST_ID}Vulnerabilities` as const; /* Threat intelligence */ diff --git a/x-pack/plugins/security_solution/public/flyout/document_details/right/components/user_entity_overview.test.tsx b/x-pack/plugins/security_solution/public/flyout/document_details/right/components/user_entity_overview.test.tsx index 000da8946ff61..95c399ca4362e 100644 --- a/x-pack/plugins/security_solution/public/flyout/document_details/right/components/user_entity_overview.test.tsx +++ b/x-pack/plugins/security_solution/public/flyout/document_details/right/components/user_entity_overview.test.tsx @@ -7,6 +7,7 @@ import React from 'react'; import { render } from '@testing-library/react'; import { TestProviders } from '../../../../common/mock'; +import { useMisconfigurationPreview } from '@kbn/cloud-security-posture/src/hooks/use_misconfiguration_preview'; import { UserEntityOverview, USER_PREVIEW_BANNER } from './user_entity_overview'; import { useFirstLastSeen } from '../../../../common/containers/use_first_last_seen'; import { @@ -15,6 +16,8 @@ import { ENTITIES_USER_OVERVIEW_LINK_TEST_ID, ENTITIES_USER_OVERVIEW_RISK_LEVEL_TEST_ID, ENTITIES_USER_OVERVIEW_LOADING_TEST_ID, + ENTITIES_USER_OVERVIEW_MISCONFIGURATIONS_TEST_ID, + ENTITIES_USER_OVERVIEW_ALERT_COUNT_TEST_ID, } from './test_ids'; import { useObservedUserDetails } from '../../../../explore/users/containers/users/observed_details'; import { mockContextValue } from '../../shared/mocks/mock_context'; @@ -28,6 +31,7 @@ import { useExpandableFlyoutApi } from '@kbn/expandable-flyout'; import { useIsExperimentalFeatureEnabled } from '../../../../common/hooks/use_experimental_features'; import { mockFlyoutApi } from '../../shared/mocks/mock_flyout_context'; import { UserPreviewPanelKey } from '../../../entity_details/user_right'; +import { useSummaryChartData } from '../../../../detections/components/alerts_kpis/alerts_summary_charts_panel/use_summary_chart_data'; const userName = 'user'; const domain = 'n54bg2lfc7'; @@ -45,6 +49,18 @@ const panelContextValue = { }; jest.mock('@kbn/expandable-flyout'); +jest.mock('@kbn/cloud-security-posture/src/hooks/use_misconfiguration_preview'); + +jest.mock('../../../../common/lib/kibana'); + +jest.mock('react-router-dom', () => { + const actual = jest.requireActual('react-router-dom'); + return { ...actual, useLocation: jest.fn().mockReturnValue({ pathname: '' }) }; +}); + +jest.mock( + '../../../../detections/components/alerts_kpis/alerts_summary_charts_panel/use_summary_chart_data' +); jest.mock('../../../../common/hooks/use_experimental_features'); const mockUseIsExperimentalFeatureEnabled = useIsExperimentalFeatureEnabled as jest.Mock; @@ -85,6 +101,8 @@ describe('', () => { beforeAll(() => { jest.mocked(useExpandableFlyoutApi).mockReturnValue(mockFlyoutApi); mockUseIsExperimentalFeatureEnabled.mockReturnValue(true); + (useMisconfigurationPreview as jest.Mock).mockReturnValue({}); + (useSummaryChartData as jest.Mock).mockReturnValue({ isLoading: false, items: [] }); }); describe('license is valid', () => { @@ -211,4 +229,38 @@ describe('', () => { }); }); }); + + describe('distribution bar insights', () => { + beforeEach(() => { + mockUseUserDetails.mockReturnValue([false, { userDetails: userData }]); + mockUseRiskScore.mockReturnValue({ data: riskLevel, isAuthorized: true }); + }); + + it('should not render if no data is available', () => { + const { queryByTestId } = renderUserEntityOverview(); + expect( + queryByTestId(ENTITIES_USER_OVERVIEW_MISCONFIGURATIONS_TEST_ID) + ).not.toBeInTheDocument(); + expect(queryByTestId(ENTITIES_USER_OVERVIEW_ALERT_COUNT_TEST_ID)).not.toBeInTheDocument(); + }); + + it('should render alert count when data is available', () => { + (useSummaryChartData as jest.Mock).mockReturnValue({ + isLoading: false, + items: [{ key: 'high', value: 78, label: 'High' }], + }); + + const { getByTestId } = renderUserEntityOverview(); + expect(getByTestId(ENTITIES_USER_OVERVIEW_ALERT_COUNT_TEST_ID)).toBeInTheDocument(); + }); + + it('should render misconfiguration when data is available', () => { + (useMisconfigurationPreview as jest.Mock).mockReturnValue({ + data: { count: { passed: 1, failed: 2 } }, + }); + + const { getByTestId } = renderUserEntityOverview(); + expect(getByTestId(ENTITIES_USER_OVERVIEW_MISCONFIGURATIONS_TEST_ID)).toBeInTheDocument(); + }); + }); }); diff --git a/x-pack/plugins/security_solution/public/flyout/document_details/right/components/user_entity_overview.tsx b/x-pack/plugins/security_solution/public/flyout/document_details/right/components/user_entity_overview.tsx index 624b9e816c9e5..0019228d656cd 100644 --- a/x-pack/plugins/security_solution/public/flyout/document_details/right/components/user_entity_overview.tsx +++ b/x-pack/plugins/security_solution/public/flyout/document_details/right/components/user_entity_overview.tsx @@ -53,10 +53,14 @@ import { ENTITIES_USER_OVERVIEW_RISK_LEVEL_TEST_ID, ENTITIES_USER_OVERVIEW_LINK_TEST_ID, ENTITIES_USER_OVERVIEW_LOADING_TEST_ID, + ENTITIES_USER_OVERVIEW_MISCONFIGURATIONS_TEST_ID, + ENTITIES_USER_OVERVIEW_ALERT_COUNT_TEST_ID, } from './test_ids'; import { useObservedUserDetails } from '../../../../explore/users/containers/users/observed_details'; import { RiskScoreDocTooltip } from '../../../../overview/components/common'; import { PreviewLink } from '../../../shared/components/preview_link'; +import { MisconfigurationsInsight } from '../../shared/components/misconfiguration_insight'; +import { AlertCountInsight } from '../../shared/components/alert_count_insight'; const USER_ICON = 'user'; @@ -196,12 +200,12 @@ export const UserEntityOverview: React.FC = ({ userName return ( - + @@ -270,6 +274,16 @@ export const UserEntityOverview: React.FC = ({ userName )} + + ); }; diff --git a/x-pack/plugins/security_solution/public/flyout/document_details/shared/components/alert_count_insight.test.tsx b/x-pack/plugins/security_solution/public/flyout/document_details/shared/components/alert_count_insight.test.tsx new file mode 100644 index 0000000000000..f0d16a418f2b2 --- /dev/null +++ b/x-pack/plugins/security_solution/public/flyout/document_details/shared/components/alert_count_insight.test.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 { render } from '@testing-library/react'; +import { TestProviders } from '../../../../common/mock'; +import { AlertCountInsight } from './alert_count_insight'; +import { useSummaryChartData } from '../../../../detections/components/alerts_kpis/alerts_summary_charts_panel/use_summary_chart_data'; + +jest.mock('../../../../common/lib/kibana'); + +jest.mock('react-router-dom', () => { + const actual = jest.requireActual('react-router-dom'); + return { ...actual, useLocation: jest.fn().mockReturnValue({ pathname: '' }) }; +}); +jest.mock('@kbn/cloud-security-posture/src/hooks/use_misconfiguration_preview'); +jest.mock( + '../../../../detections/components/alerts_kpis/alerts_summary_charts_panel/use_summary_chart_data' +); + +const fieldName = 'host.name'; +const name = 'test host'; +const testId = 'test'; + +const renderAlertCountInsight = () => { + return render( + + + + ); +}; + +describe('AlertCountInsight', () => { + it('renders', () => { + (useSummaryChartData as jest.Mock).mockReturnValue({ + isLoading: false, + items: [ + { key: 'high', value: 78, label: 'High' }, + { key: 'low', value: 46, label: 'Low' }, + { key: 'medium', value: 32, label: 'Medium' }, + { key: 'critical', value: 21, label: 'Critical' }, + ], + }); + const { getByTestId } = renderAlertCountInsight(); + expect(getByTestId(testId)).toBeInTheDocument(); + expect(getByTestId(`${testId}-distribution-bar`)).toBeInTheDocument(); + }); + + it('renders loading spinner if data is being fetched', () => { + (useSummaryChartData as jest.Mock).mockReturnValue({ isLoading: true, items: [] }); + const { getByTestId } = renderAlertCountInsight(); + expect(getByTestId(`${testId}-loading-spinner`)).toBeInTheDocument(); + }); + + it('renders null if no misconfiguration data found', () => { + (useSummaryChartData as jest.Mock).mockReturnValue({ isLoading: false, items: [] }); + const { container } = renderAlertCountInsight(); + expect(container).toBeEmptyDOMElement(); + }); +}); diff --git a/x-pack/plugins/security_solution/public/flyout/document_details/shared/components/alert_count_insight.tsx b/x-pack/plugins/security_solution/public/flyout/document_details/shared/components/alert_count_insight.tsx new file mode 100644 index 0000000000000..566b77b5739a9 --- /dev/null +++ b/x-pack/plugins/security_solution/public/flyout/document_details/shared/components/alert_count_insight.tsx @@ -0,0 +1,99 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React, { useMemo } from 'react'; +import { v4 as uuid } from 'uuid'; +import { EuiLoadingSpinner, EuiFlexItem, type EuiFlexGroupProps } from '@elastic/eui'; +import { FormattedMessage } from '@kbn/i18n-react'; +import { InsightDistributionBar } from './insight_distribution_bar'; +import { severityAggregations } from '../../../../detections/components/alerts_kpis/alerts_summary_charts_panel/aggregations'; +import { useSummaryChartData } from '../../../../detections/components/alerts_kpis/alerts_summary_charts_panel/use_summary_chart_data'; +import { + getIsAlertsBySeverityData, + getSeverityColor, +} from '../../../../detections/components/alerts_kpis/severity_level_panel/helpers'; + +const ENTITY_ALERT_COUNT_ID = 'entity-alert-count'; + +interface AlertCountInsightProps { + /** + * The name of the entity to filter the alerts by. + */ + name: string; + /** + * The field name to filter the alerts by. + */ + fieldName: 'host.name' | 'user.name'; + /** + * The direction of the flex group. + */ + direction?: EuiFlexGroupProps['direction']; + /** + * The data-test-subj to use for the component. + */ + ['data-test-subj']?: string; +} + +/* + * Displays a distribution bar with the count of critical alerts for a given entity + */ +export const AlertCountInsight: React.FC = ({ + name, + fieldName, + direction, + 'data-test-subj': dataTestSubj, +}) => { + const uniqueQueryId = useMemo(() => `${ENTITY_ALERT_COUNT_ID}-${uuid()}`, []); + const entityFilter = useMemo(() => ({ field: fieldName, value: name }), [fieldName, name]); + + const { items, isLoading } = useSummaryChartData({ + aggregations: severityAggregations, + entityFilter, + uniqueQueryId, + signalIndexName: null, + }); + + const data = useMemo(() => (getIsAlertsBySeverityData(items) ? items : []), [items]); + + const alertStats = useMemo(() => { + return data.map((item) => ({ + key: item.key, + count: item.value, + color: getSeverityColor(item.key), + })); + }, [data]); + + const count = useMemo( + () => data.filter((item) => item.key === 'critical')[0]?.value ?? 0, + [data] + ); + + if (!isLoading && items.length === 0) return null; + + return ( + + {isLoading ? ( + + ) : ( + + } + stats={alertStats} + count={count} + direction={direction} + data-test-subj={`${dataTestSubj}-distribution-bar`} + /> + )} + + ); +}; + +AlertCountInsight.displayName = 'AlertCountInsight'; diff --git a/x-pack/plugins/security_solution/public/flyout/document_details/shared/components/insight_distribution_bar.test.tsx b/x-pack/plugins/security_solution/public/flyout/document_details/shared/components/insight_distribution_bar.test.tsx new file mode 100644 index 0000000000000..a775da8a7f73a --- /dev/null +++ b/x-pack/plugins/security_solution/public/flyout/document_details/shared/components/insight_distribution_bar.test.tsx @@ -0,0 +1,41 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React from 'react'; +import { render } from '@testing-library/react'; +import { InsightDistributionBar } from './insight_distribution_bar'; +import { TestProviders } from '../../../../common/mock'; + +const title = 'test title'; +const count = 10; +const testId = 'test-id'; +const stats = [ + { + key: 'passed', + count: 90, + color: 'green', + }, + { + key: 'failed', + count: 10, + color: 'red', + }, +]; + +describe('', () => { + it('should render', () => { + const { getByTestId, getByText } = render( + + + + ); + expect(getByTestId(testId)).toBeInTheDocument(); + expect(getByText(title)).toBeInTheDocument(); + expect(getByTestId(`${testId}-badge`)).toHaveTextContent(`${count}`); + expect(getByTestId(`${testId}-distribution-bar`)).toBeInTheDocument(); + }); +}); diff --git a/x-pack/plugins/security_solution/public/flyout/document_details/shared/components/insight_distribution_bar.tsx b/x-pack/plugins/security_solution/public/flyout/document_details/shared/components/insight_distribution_bar.tsx new file mode 100644 index 0000000000000..006ec8c5dad4f --- /dev/null +++ b/x-pack/plugins/security_solution/public/flyout/document_details/shared/components/insight_distribution_bar.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 from 'react'; +import { css } from '@emotion/css'; +import { + EuiFlexGroup, + EuiFlexItem, + EuiText, + EuiBadge, + useEuiTheme, + useEuiFontSize, + type EuiFlexGroupProps, +} from '@elastic/eui'; +import { DistributionBar } from '@kbn/security-solution-distribution-bar'; +import { FormattedCount } from '../../../../common/components/formatted_number'; + +export interface InsightDistributionBarProps { + /** + * Title of the insight + */ + title: string | React.ReactNode; + /** + * Distribution stats to display + */ + stats: Array<{ key: string; count: number; color: string; label?: React.ReactNode }>; + /** + * Count to be displayed in the badge + */ + count: number; + /** + * Flex direction of the component + */ + direction?: EuiFlexGroupProps['direction']; + /** + * Optional test id + */ + ['data-test-subj']?: string; +} + +// Displays a distribution bar with a count badge +export const InsightDistributionBar: React.FC = ({ + title, + stats, + count, + direction = 'row', + 'data-test-subj': dataTestSubj, +}) => { + const { euiTheme } = useEuiTheme(); + const xsFontSize = useEuiFontSize('xs').fontSize; + + return ( + + + + {title} + + + + + + + + + + + + + + + + ); +}; + +InsightDistributionBar.displayName = 'InsightDistributionBar'; diff --git a/x-pack/plugins/security_solution/public/flyout/document_details/shared/components/misconfiguration_insight.test.tsx b/x-pack/plugins/security_solution/public/flyout/document_details/shared/components/misconfiguration_insight.test.tsx new file mode 100644 index 0000000000000..296a61f444a17 --- /dev/null +++ b/x-pack/plugins/security_solution/public/flyout/document_details/shared/components/misconfiguration_insight.test.tsx @@ -0,0 +1,43 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React from 'react'; +import { render } from '@testing-library/react'; +import { TestProviders } from '../../../../common/mock'; +import { MisconfigurationsInsight } from './misconfiguration_insight'; +import { useMisconfigurationPreview } from '@kbn/cloud-security-posture/src/hooks/use_misconfiguration_preview'; + +jest.mock('@kbn/cloud-security-posture/src/hooks/use_misconfiguration_preview'); + +const fieldName = 'host.name'; +const name = 'test host'; +const testId = 'test'; + +const renderMisconfigurationsInsight = () => { + return render( + + + + ); +}; + +describe('MisconfigurationsInsight', () => { + it('renders', () => { + (useMisconfigurationPreview as jest.Mock).mockReturnValue({ + data: { count: { passed: 1, failed: 2 } }, + }); + const { getByTestId } = renderMisconfigurationsInsight(); + expect(getByTestId(testId)).toBeInTheDocument(); + expect(getByTestId(`${testId}-distribution-bar`)).toBeInTheDocument(); + }); + + it('renders null if no misconfiguration data found', () => { + (useMisconfigurationPreview as jest.Mock).mockReturnValue({}); + const { container } = renderMisconfigurationsInsight(); + expect(container).toBeEmptyDOMElement(); + }); +}); diff --git a/x-pack/plugins/security_solution/public/flyout/document_details/shared/components/misconfiguration_insight.tsx b/x-pack/plugins/security_solution/public/flyout/document_details/shared/components/misconfiguration_insight.tsx new file mode 100644 index 0000000000000..552a242c84893 --- /dev/null +++ b/x-pack/plugins/security_solution/public/flyout/document_details/shared/components/misconfiguration_insight.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; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React, { useMemo } from 'react'; +import { EuiFlexItem, type EuiFlexGroupProps } from '@elastic/eui'; +import { FormattedMessage } from '@kbn/i18n-react'; +import { useMisconfigurationPreview } from '@kbn/cloud-security-posture/src/hooks/use_misconfiguration_preview'; +import { buildEntityFlyoutPreviewQuery } from '@kbn/cloud-security-posture-common'; +import { InsightDistributionBar } from './insight_distribution_bar'; +import { getFindingsStats } from '../../../../cloud_security_posture/components/misconfiguration/misconfiguration_preview'; + +interface MisconfigurationsInsightProps { + /** + * Entity name to retrieve misconfigurations for + */ + name: string; + /** + * Indicator whether the entity is host or user + */ + fieldName: 'host.name' | 'user.name'; + /** + * The direction of the flex group + */ + direction?: EuiFlexGroupProps['direction']; + /** + * The data-test-subj to use for the component + */ + ['data-test-subj']?: string; +} + +/* + * Displays a distribution bar with the count of failed misconfigurations for a given entity + */ +export const MisconfigurationsInsight: React.FC = ({ + name, + fieldName, + direction, + 'data-test-subj': dataTestSubj, +}) => { + const { data } = useMisconfigurationPreview({ + query: buildEntityFlyoutPreviewQuery(fieldName, name), + sort: [], + enabled: true, + pageSize: 1, + }); + + const passedFindings = data?.count.passed || 0; + const failedFindings = data?.count.failed || 0; + const hasMisconfigurationFindings = passedFindings > 0 || failedFindings > 0; + + const misconfigurationsStats = useMemo( + () => getFindingsStats(passedFindings, failedFindings), + [passedFindings, failedFindings] + ); + + if (!hasMisconfigurationFindings) return null; + + return ( + + + } + stats={misconfigurationsStats} + count={failedFindings} + direction={direction} + data-test-subj={`${dataTestSubj}-distribution-bar`} + /> + + ); +}; + +MisconfigurationsInsight.displayName = 'MisconfigurationsInsight'; diff --git a/x-pack/plugins/security_solution/public/flyout/document_details/shared/components/test_ids.ts b/x-pack/plugins/security_solution/public/flyout/document_details/shared/components/test_ids.ts index 8561df63d7199..7c2ce2ff5870b 100644 --- a/x-pack/plugins/security_solution/public/flyout/document_details/shared/components/test_ids.ts +++ b/x-pack/plugins/security_solution/public/flyout/document_details/shared/components/test_ids.ts @@ -12,3 +12,6 @@ export const FLYOUT_PREVIEW_LINK_TEST_ID = `${PREFIX}PreviewLink` as const; export const SESSION_VIEW_UPSELL_TEST_ID = `${PREFIX}SessionViewUpsell` as const; export const SESSION_VIEW_NO_DATA_TEST_ID = `${PREFIX}SessionViewNoData` as const; + +export const MISCONFIGURATIONS_TEST_ID = `${PREFIX}Misconfigurations` as const; +export const VULNERABILITIES_TEST_ID = `${PREFIX}Vulnerabilities` as const; diff --git a/x-pack/plugins/security_solution/public/flyout/document_details/shared/components/vulnerabilities_insight.test.tsx b/x-pack/plugins/security_solution/public/flyout/document_details/shared/components/vulnerabilities_insight.test.tsx new file mode 100644 index 0000000000000..77c6737266b89 --- /dev/null +++ b/x-pack/plugins/security_solution/public/flyout/document_details/shared/components/vulnerabilities_insight.test.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 { TestProviders } from '../../../../common/mock'; +import { render } from '@testing-library/react'; +import React from 'react'; +import { VulnerabilitiesInsight } from './vulnerabilities_insight'; +import { useVulnerabilitiesPreview } from '@kbn/cloud-security-posture/src/hooks/use_vulnerabilities_preview'; + +jest.mock('@kbn/cloud-security-posture/src/hooks/use_vulnerabilities_preview'); + +const hostName = 'test host'; +const testId = 'test'; + +const renderVulnerabilitiesInsight = () => { + return render( + + + + ); +}; + +describe('VulnerabilitiesInsight', () => { + it('renders', () => { + (useVulnerabilitiesPreview as jest.Mock).mockReturnValue({ + data: { count: { CRITICAL: 0, HIGH: 1, MEDIUM: 1, LOW: 0, UNKNOWN: 0 } }, + }); + + const { getByTestId } = renderVulnerabilitiesInsight(); + expect(getByTestId(testId)).toBeInTheDocument(); + expect(getByTestId(`${testId}-distribution-bar`)).toBeInTheDocument(); + }); + + it('renders null when data is not available', () => { + (useVulnerabilitiesPreview as jest.Mock).mockReturnValue({}); + + const { container } = renderVulnerabilitiesInsight(); + expect(container).toBeEmptyDOMElement(); + }); +}); diff --git a/x-pack/plugins/security_solution/public/flyout/document_details/shared/components/vulnerabilities_insight.tsx b/x-pack/plugins/security_solution/public/flyout/document_details/shared/components/vulnerabilities_insight.tsx new file mode 100644 index 0000000000000..4c581b6db57d0 --- /dev/null +++ b/x-pack/plugins/security_solution/public/flyout/document_details/shared/components/vulnerabilities_insight.tsx @@ -0,0 +1,91 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React, { useMemo } from 'react'; +import { EuiFlexItem, type EuiFlexGroupProps } from '@elastic/eui'; +import { FormattedMessage } from '@kbn/i18n-react'; +import { useVulnerabilitiesPreview } from '@kbn/cloud-security-posture/src/hooks/use_vulnerabilities_preview'; +import { buildEntityFlyoutPreviewQuery } from '@kbn/cloud-security-posture-common'; +import { getVulnerabilityStats, hasVulnerabilitiesData } from '@kbn/cloud-security-posture'; +import { InsightDistributionBar } from './insight_distribution_bar'; + +interface VulnerabilitiesInsightProps { + /** + * Host name to retrieve vulnerabilities for + */ + hostName: string; + /** + * The direction of the flex group + */ + direction?: EuiFlexGroupProps['direction']; + /** + * The data-test-subj to use for the component + */ + ['data-test-subj']?: string; +} + +/* + * Displays a distribution bar with the count of critical vulnerabilities for a given host + */ +export const VulnerabilitiesInsight: React.FC = ({ + hostName, + direction, + 'data-test-subj': dataTestSubj, +}) => { + const { data } = useVulnerabilitiesPreview({ + query: buildEntityFlyoutPreviewQuery('host.name', hostName), + sort: [], + enabled: true, + pageSize: 1, + }); + + const { CRITICAL = 0, HIGH = 0, MEDIUM = 0, LOW = 0, NONE = 0 } = data?.count || {}; + const hasVulnerabilitiesFindings = useMemo( + () => + hasVulnerabilitiesData({ + critical: CRITICAL, + high: HIGH, + medium: MEDIUM, + low: LOW, + none: NONE, + }), + [CRITICAL, HIGH, MEDIUM, LOW, NONE] + ); + + const vulnerabilitiesStats = useMemo( + () => + getVulnerabilityStats({ + critical: CRITICAL, + high: HIGH, + medium: MEDIUM, + low: LOW, + none: NONE, + }), + [CRITICAL, HIGH, MEDIUM, LOW, NONE] + ); + + if (!hasVulnerabilitiesFindings) return null; + + return ( + + + } + stats={vulnerabilitiesStats} + count={CRITICAL} + direction={direction} + data-test-subj={`${dataTestSubj}-distribution-bar`} + /> + + ); +}; + +VulnerabilitiesInsight.displayName = 'VulnerabilitiesInsight'; diff --git a/x-pack/plugins/security_solution/public/flyout/entity_details/host_details_left/index.tsx b/x-pack/plugins/security_solution/public/flyout/entity_details/host_details_left/index.tsx index 4d7ffc641ffc7..6e5774ba1756e 100644 --- a/x-pack/plugins/security_solution/public/flyout/entity_details/host_details_left/index.tsx +++ b/x-pack/plugins/security_solution/public/flyout/entity_details/host_details_left/index.tsx @@ -12,6 +12,7 @@ import { getInsightsInputTab, } from '../../../entity_analytics/components/entity_details_flyout'; import { LeftPanelContent } from '../shared/components/left_panel/left_panel_content'; +import type { CspInsightLeftPanelSubTab } from '../shared/components/left_panel/left_panel_header'; import { EntityDetailsLeftPanelTab, LeftPanelHeader, @@ -23,8 +24,10 @@ export interface HostDetailsPanelProps extends Record { name: string; scopeId: string; hasMisconfigurationFindings?: boolean; + hasVulnerabilitiesFindings?: boolean; path?: { tab?: EntityDetailsLeftPanelTab; + subTab?: CspInsightLeftPanelSubTab; }; } export interface HostDetailsExpandableFlyoutProps extends FlyoutPanelProps { @@ -39,6 +42,7 @@ export const HostDetailsPanel = ({ scopeId, path, hasMisconfigurationFindings, + hasVulnerabilitiesFindings, }: HostDetailsPanelProps) => { const [selectedTabId, setSelectedTabId] = useState( path?.tab === EntityDetailsLeftPanelTab.CSP_INSIGHTS @@ -53,11 +57,12 @@ export const HostDetailsPanel = ({ : []; // Determine if the Insights tab should be included - const insightsTab = hasMisconfigurationFindings - ? [getInsightsInputTab({ name, fieldName: 'host.name' })] - : []; + const insightsTab = + hasMisconfigurationFindings || hasVulnerabilitiesFindings + ? [getInsightsInputTab({ name, fieldName: 'host.name' })] + : []; return [[...riskScoreTab, ...insightsTab], EntityDetailsLeftPanelTab.RISK_INPUTS, () => {}]; - }, [isRiskScoreExist, name, scopeId, hasMisconfigurationFindings]); + }, [isRiskScoreExist, name, scopeId, hasMisconfigurationFindings, hasVulnerabilitiesFindings]); return ( <> diff --git a/x-pack/plugins/security_solution/public/flyout/entity_details/host_right/content.tsx b/x-pack/plugins/security_solution/public/flyout/entity_details/host_right/content.tsx index 8ea65a7f3096b..4538f53f0bd81 100644 --- a/x-pack/plugins/security_solution/public/flyout/entity_details/host_right/content.tsx +++ b/x-pack/plugins/security_solution/public/flyout/entity_details/host_right/content.tsx @@ -8,7 +8,7 @@ import React from 'react'; import { EuiHorizontalRule } from '@elastic/eui'; import { FlyoutBody } from '@kbn/security-solution-common'; -import { EntityInsight } from '../../../cloud_security_posture/components'; +import { EntityInsight } from '../../../cloud_security_posture/components/entity_insight'; import { AssetCriticalityAccordion } from '../../../entity_analytics/components/asset_criticality/asset_criticality_selector'; import { FlyoutRiskSummary } from '../../../entity_analytics/components/risk_summary_flyout/risk_summary'; import type { RiskScoreState } from '../../../entity_analytics/api/hooks/use_risk_score'; diff --git a/x-pack/plugins/security_solution/public/flyout/entity_details/host_right/index.tsx b/x-pack/plugins/security_solution/public/flyout/entity_details/host_right/index.tsx index 42280e60ef46e..9c75287ed0657 100644 --- a/x-pack/plugins/security_solution/public/flyout/entity_details/host_right/index.tsx +++ b/x-pack/plugins/security_solution/public/flyout/entity_details/host_right/index.tsx @@ -12,6 +12,8 @@ import { useExpandableFlyoutApi } from '@kbn/expandable-flyout'; import { FlyoutLoading, FlyoutNavigation } from '@kbn/security-solution-common'; import { buildEntityFlyoutPreviewQuery } from '@kbn/cloud-security-posture-common'; import { useMisconfigurationPreview } from '@kbn/cloud-security-posture/src/hooks/use_misconfiguration_preview'; +import { useVulnerabilitiesPreview } from '@kbn/cloud-security-posture/src/hooks/use_vulnerabilities_preview'; +import { sum } from 'lodash'; import { useRefetchQueryById } from '../../../entity_analytics/api/hooks/use_refetch_query_by_id'; import { RISK_INPUTS_TAB_QUERY_ID } from '../../../entity_analytics/components/entity_details_flyout/tabs/risk_inputs/risk_inputs_tab'; import type { Refetch } from '../../../common/types'; @@ -107,6 +109,15 @@ export const HostPanel = ({ const hasMisconfigurationFindings = passedFindings > 0 || failedFindings > 0; + const { data: vulnerabilitiesData } = useVulnerabilitiesPreview({ + query: buildEntityFlyoutPreviewQuery('host.name', hostName), + sort: [], + enabled: true, + pageSize: 1, + }); + + const hasVulnerabilitiesFindings = sum(Object.values(vulnerabilitiesData?.count || {})) > 0; + useQueryInspector({ deleteQuery, inspect: inspectRiskScore, @@ -130,10 +141,19 @@ export const HostPanel = ({ isRiskScoreExist, path: tab ? { tab } : undefined, hasMisconfigurationFindings, + hasVulnerabilitiesFindings, }, }); }, - [telemetry, openLeftPanel, hostName, scopeId, isRiskScoreExist, hasMisconfigurationFindings] + [ + telemetry, + openLeftPanel, + hostName, + scopeId, + isRiskScoreExist, + hasMisconfigurationFindings, + hasVulnerabilitiesFindings, + ] ); const openDefaultPanel = useCallback( @@ -173,7 +193,8 @@ export const HostPanel = ({ <> diff --git a/x-pack/plugins/security_solution/public/flyout/entity_details/shared/components/left_panel/left_panel_header.tsx b/x-pack/plugins/security_solution/public/flyout/entity_details/shared/components/left_panel/left_panel_header.tsx index a33911c928aaf..438f75e7a4ccb 100644 --- a/x-pack/plugins/security_solution/public/flyout/entity_details/shared/components/left_panel/left_panel_header.tsx +++ b/x-pack/plugins/security_solution/public/flyout/entity_details/shared/components/left_panel/left_panel_header.tsx @@ -25,6 +25,11 @@ export enum EntityDetailsLeftPanelTab { CSP_INSIGHTS = 'csp_insights', } +export enum CspInsightLeftPanelSubTab { + MISCONFIGURATIONS = 'misconfigurationTabId', + VULNERABILITIES = 'vulnerabilitiesTabId', +} + export interface PanelHeaderProps { /** * Id of the tab selected in the parent component to display its content diff --git a/x-pack/plugins/security_solution/public/flyout/entity_details/user_right/content.tsx b/x-pack/plugins/security_solution/public/flyout/entity_details/user_right/content.tsx index 42b281d0c8d2b..8bcf5dd690200 100644 --- a/x-pack/plugins/security_solution/public/flyout/entity_details/user_right/content.tsx +++ b/x-pack/plugins/security_solution/public/flyout/entity_details/user_right/content.tsx @@ -23,7 +23,7 @@ import { ObservedEntity } from '../shared/components/observed_entity'; import type { ObservedEntityData } from '../shared/components/observed_entity/types'; import { useObservedUserItems } from './hooks/use_observed_user_items'; import type { EntityDetailsLeftPanelTab } from '../shared/components/left_panel/left_panel_header'; -import { EntityInsight } from '../../../cloud_security_posture/components'; +import { EntityInsight } from '../../../cloud_security_posture/components/entity_insight'; interface UserPanelContentProps { userName: string; diff --git a/x-pack/plugins/security_solution/public/flyout/entity_details/user_right/index.tsx b/x-pack/plugins/security_solution/public/flyout/entity_details/user_right/index.tsx index ee1ec526c2840..ec55fb292abfd 100644 --- a/x-pack/plugins/security_solution/public/flyout/entity_details/user_right/index.tsx +++ b/x-pack/plugins/security_solution/public/flyout/entity_details/user_right/index.tsx @@ -30,7 +30,7 @@ import { UserPanelContent } from './content'; import { UserPanelHeader } from './header'; import { UserDetailsPanelKey } from '../user_details_left'; import { useObservedUser } from './hooks/use_observed_user'; -import type { EntityDetailsLeftPanelTab } from '../shared/components/left_panel/left_panel_header'; +import { EntityDetailsLeftPanelTab } from '../shared/components/left_panel/left_panel_header'; import { UserPreviewPanelFooter } from '../user_preview/footer'; export interface UserPanelProps extends Record { @@ -83,6 +83,7 @@ export const UserPanel = ({ const { data: userRisk } = riskScoreState; const userRiskData = userRisk && userRisk.length > 0 ? userRisk[0] : undefined; + const isRiskScoreExist = !!userRiskData?.user.risk; const refetchRiskInputsTab = useRefetchQueryById(RISK_INPUTS_TAB_QUERY_ID); const refetchRiskScore = useCallback(() => { @@ -149,8 +150,15 @@ export const UserPanel = ({ hasMisconfigurationFindings, ] ); - - const openPanelFirstTab = useCallback(() => openPanelTab(), [openPanelTab]); + const openPanelFirstTab = useCallback( + () => + openPanelTab( + isRiskScoreExist + ? EntityDetailsLeftPanelTab.RISK_INPUTS + : EntityDetailsLeftPanelTab.CSP_INSIGHTS + ), + [isRiskScoreExist, openPanelTab] + ); const hasUserDetailsData = !!userRiskData?.user.risk || diff --git a/x-pack/plugins/security_solution/public/flyout/rule_details/preview/footer.test.tsx b/x-pack/plugins/security_solution/public/flyout/rule_details/preview/footer.test.tsx index f1e276011ca26..0f2a7dc74662f 100644 --- a/x-pack/plugins/security_solution/public/flyout/rule_details/preview/footer.test.tsx +++ b/x-pack/plugins/security_solution/public/flyout/rule_details/preview/footer.test.tsx @@ -9,20 +9,21 @@ import { render } from '@testing-library/react'; import React from 'react'; import { RULE_PREVIEW_FOOTER_TEST_ID, RULE_PREVIEW_OPEN_RULE_FLYOUT_TEST_ID } from './test_ids'; import { PreviewFooter } from './footer'; -import { mockFlyoutApi } from '../../document_details/shared/mocks/mock_flyout_context'; -import { useExpandableFlyoutApi } from '@kbn/expandable-flyout'; -import { RulePanelKey } from '../right'; +import { useRuleDetailsLink } from '../../document_details/shared/hooks/use_rule_details_link'; +import { TestProviders } from '../../../common/mock'; -jest.mock('@kbn/expandable-flyout'); +jest.mock('../../document_details/shared/hooks/use_rule_details_link'); -const renderRulePreviewFooter = () => render(); +const renderRulePreviewFooter = () => + render( + + + + ); describe('', () => { - beforeAll(() => { - jest.mocked(useExpandableFlyoutApi).mockReturnValue(mockFlyoutApi); - }); - it('should render rule details link correctly when ruleId is available', () => { + (useRuleDetailsLink as jest.Mock).mockReturnValue('rule_details_link'); const { getByTestId } = renderRulePreviewFooter(); expect(getByTestId(RULE_PREVIEW_FOOTER_TEST_ID)).toBeInTheDocument(); @@ -32,13 +33,9 @@ describe('', () => { ); }); - it('should open rule flyout when clicked', () => { - const { getByTestId } = renderRulePreviewFooter(); - - getByTestId(RULE_PREVIEW_OPEN_RULE_FLYOUT_TEST_ID).click(); - - expect(mockFlyoutApi.openFlyout).toHaveBeenCalledWith({ - right: { id: RulePanelKey, params: { ruleId: 'ruleid' } }, - }); + it('should not render the footer if rule link is not available', () => { + (useRuleDetailsLink as jest.Mock).mockReturnValue(null); + const { container } = renderRulePreviewFooter(); + expect(container).toBeEmptyDOMElement(); }); }); diff --git a/x-pack/plugins/security_solution/public/flyout/rule_details/preview/footer.tsx b/x-pack/plugins/security_solution/public/flyout/rule_details/preview/footer.tsx index 1774c37d9e535..42c8c1a6d85b9 100644 --- a/x-pack/plugins/security_solution/public/flyout/rule_details/preview/footer.tsx +++ b/x-pack/plugins/security_solution/public/flyout/rule_details/preview/footer.tsx @@ -5,38 +5,27 @@ * 2.0. */ -import React, { memo, useCallback } from 'react'; +import React, { memo } from 'react'; import { EuiFlexGroup, EuiFlexItem, EuiLink } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import { FlyoutFooter } from '@kbn/security-solution-common'; -import { useExpandableFlyoutApi } from '@kbn/expandable-flyout'; import { RULE_PREVIEW_FOOTER_TEST_ID, RULE_PREVIEW_OPEN_RULE_FLYOUT_TEST_ID } from './test_ids'; -import { RulePanelKey } from '../right'; +import { useRuleDetailsLink } from '../../document_details/shared/hooks/use_rule_details_link'; /** * Footer in rule preview panel */ export const PreviewFooter = memo(({ ruleId }: { ruleId: string }) => { - const { openFlyout } = useExpandableFlyoutApi(); + const href = useRuleDetailsLink({ ruleId }); - const openRuleFlyout = useCallback(() => { - openFlyout({ - right: { - id: RulePanelKey, - params: { - ruleId, - }, - }, - }); - }, [openFlyout, ruleId]); - - return ( + return href ? ( {i18n.translate('xpack.securitySolution.flyout.preview.rule.viewDetailsLabel', { @@ -46,7 +35,7 @@ export const PreviewFooter = memo(({ ruleId }: { ruleId: string }) => { - ); + ) : null; }); PreviewFooter.displayName = 'PreviewFooter'; diff --git a/x-pack/plugins/security_solution/public/flyout/rule_details/right/index.test.tsx b/x-pack/plugins/security_solution/public/flyout/rule_details/right/index.test.tsx index 1ce755575450c..146da2be34346 100644 --- a/x-pack/plugins/security_solution/public/flyout/rule_details/right/index.test.tsx +++ b/x-pack/plugins/security_solution/public/flyout/rule_details/right/index.test.tsx @@ -10,7 +10,7 @@ import { render } from '@testing-library/react'; import { ThemeProvider } from 'styled-components'; import { getMockTheme } from '../../../common/lib/kibana/kibana_react.mock'; import { TestProviders } from '../../../common/mock'; -// import { TestProvider } from '@kbn/expandable-flyout/src/test/provider'; +import { useRuleDetailsLink } from '../../document_details/shared/hooks/use_rule_details_link'; import { RulePanel } from '.'; import { getStepsData } from '../../../detections/pages/detection_engine/rules/helpers'; import { useRuleDetails } from '../hooks/use_rule_details'; @@ -23,6 +23,8 @@ import type { RuleResponse } from '../../../../common/api/detection_engine'; import { BODY_TEST_ID, LOADING_TEST_ID } from './test_ids'; import { RULE_PREVIEW_FOOTER_TEST_ID } from '../preview/test_ids'; +jest.mock('../../document_details/shared/hooks/use_rule_details_link'); + const mockUseRuleDetails = useRuleDetails as jest.Mock; jest.mock('../hooks/use_rule_details'); @@ -89,6 +91,7 @@ describe('', () => { }); it('should render preview footer when isPreviewMode is true', () => { + (useRuleDetailsLink as jest.Mock).mockReturnValue('rule_details_link'); mockUseRuleDetails.mockReturnValue({ rule, loading: false, @@ -97,8 +100,6 @@ describe('', () => { mockGetStepsData.mockReturnValue({}); const { getByTestId } = renderRulePanel(true); - // await act(async () => { expect(getByTestId(RULE_PREVIEW_FOOTER_TEST_ID)).toBeInTheDocument(); - // }); }); }); diff --git a/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/mocks.ts b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/mocks.ts index c3cc8adc3b4fe..45ee954caa466 100644 --- a/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/mocks.ts +++ b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/mocks.ts @@ -21,7 +21,7 @@ import { BASE_POLICY_RESPONSE_ROUTE, HOST_METADATA_GET_ROUTE, HOST_METADATA_LIST_ROUTE, - METADATA_TRANSFORMS_STATUS_ROUTE, + METADATA_TRANSFORMS_STATUS_INTERNAL_ROUTE, } from '../../../../common/endpoint/constants'; import type { PendingActionsHttpMockInterface } from '../../../common/lib/endpoint/endpoint_pending_actions/mocks'; import { pendingActionsHttpMock } from '../../../common/lib/endpoint/endpoint_pending_actions/mocks'; @@ -120,7 +120,7 @@ export const failedTransformStateMock = { export const transformsHttpMocks = httpHandlerMockFactory([ { id: 'metadataTransformStats', - path: METADATA_TRANSFORMS_STATUS_ROUTE, + path: METADATA_TRANSFORMS_STATUS_INTERNAL_ROUTE, method: 'get', handler: () => failedTransformStateMock, }, diff --git a/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/store/middleware.ts b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/store/middleware.ts index 61a049d2dd99e..f91e74983e5a2 100644 --- a/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/store/middleware.ts +++ b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/store/middleware.ts @@ -21,7 +21,7 @@ import type { import { ENDPOINT_FIELDS_SEARCH_STRATEGY, HOST_METADATA_LIST_ROUTE, - METADATA_TRANSFORMS_STATUS_ROUTE, + METADATA_TRANSFORMS_STATUS_INTERNAL_ROUTE, METADATA_UNITED_INDEX, metadataCurrentIndexPattern, } from '../../../../../common/endpoint/constants'; @@ -421,8 +421,8 @@ export async function handleLoadMetadataTransformStats(http: HttpStart, store: E try { const transformStatsResponse: TransformStatsResponse = await http.get( - METADATA_TRANSFORMS_STATUS_ROUTE, - { version: '2023-10-31' } + METADATA_TRANSFORMS_STATUS_INTERNAL_ROUTE, + { version: '1' } ); dispatch({ diff --git a/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/store/mock_endpoint_result_list.ts b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/store/mock_endpoint_result_list.ts index a851d2273907d..c29e8e25ac4d0 100644 --- a/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/store/mock_endpoint_result_list.ts +++ b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/store/mock_endpoint_result_list.ts @@ -35,7 +35,7 @@ import { ENDPOINT_DEFAULT_SORT_DIRECTION, ENDPOINT_DEFAULT_SORT_FIELD, HOST_METADATA_LIST_ROUTE, - METADATA_TRANSFORMS_STATUS_ROUTE, + METADATA_TRANSFORMS_STATUS_INTERNAL_ROUTE, } from '../../../../../common/endpoint/constants'; import type { TransformStats, TransformStatsResponse } from '../types'; @@ -178,7 +178,7 @@ const endpointListApiPathHandlerMocks = ({ return pendingActionsResponseMock(); }, - [METADATA_TRANSFORMS_STATUS_ROUTE]: (): TransformStatsResponse => ({ + [METADATA_TRANSFORMS_STATUS_INTERNAL_ROUTE]: (): TransformStatsResponse => ({ count: transforms.length, transforms, }), diff --git a/x-pack/plugins/security_solution/public/notes/components/delete_confirm_modal.tsx b/x-pack/plugins/security_solution/public/notes/components/delete_confirm_modal.tsx index cba7e81b0fb2b..3c6d6da08e190 100644 --- a/x-pack/plugins/security_solution/public/notes/components/delete_confirm_modal.tsx +++ b/x-pack/plugins/security_solution/public/notes/components/delete_confirm_modal.tsx @@ -7,7 +7,7 @@ import React, { useCallback } from 'react'; import { useDispatch, useSelector } from 'react-redux'; import { EuiConfirmModal } from '@elastic/eui'; -import * as i18n from './translations'; +import { i18n } from '@kbn/i18n'; import { deleteNotes, userClosedDeleteModal, @@ -16,6 +16,25 @@ import { ReqStatus, } from '..'; +export const DELETE = i18n.translate('xpack.securitySolution.notes.management.deleteAction', { + defaultMessage: 'Delete', +}); +export const DELETE_NOTES_CONFIRM = (selectedNotes: number) => + i18n.translate('xpack.securitySolution.notes.management.deleteNotesConfirm', { + values: { selectedNotes }, + defaultMessage: + 'Are you sure you want to delete {selectedNotes} {selectedNotes, plural, one {note} other {notes}}?', + }); +export const DELETE_NOTES_CANCEL = i18n.translate( + 'xpack.securitySolution.notes.management.deleteNotesCancel', + { + defaultMessage: 'Cancel', + } +); + +/** + * Renders a confirmation modal to delete notes in the notes management page + */ export const DeleteConfirmModal = React.memo(() => { const dispatch = useDispatch(); const pendingDeleteIds = useSelector(selectNotesTablePendingDeleteIds); @@ -33,16 +52,16 @@ export const DeleteConfirmModal = React.memo(() => { return ( - {i18n.DELETE_NOTES_CONFIRM(pendingDeleteIds.length)} + {DELETE_NOTES_CONFIRM(pendingDeleteIds.length)} ); }); diff --git a/x-pack/plugins/security_solution/public/notes/components/delete_note_button.tsx b/x-pack/plugins/security_solution/public/notes/components/delete_note_button.tsx index 3f9e757d3f5a5..4744c362e469c 100644 --- a/x-pack/plugins/security_solution/public/notes/components/delete_note_button.tsx +++ b/x-pack/plugins/security_solution/public/notes/components/delete_note_button.tsx @@ -13,10 +13,10 @@ import { DELETE_NOTE_BUTTON_TEST_ID } from './test_ids'; import type { State } from '../../common/store'; import type { Note } from '../../../common/api/timeline'; import { - deleteNotes, ReqStatus, selectDeleteNotesError, selectDeleteNotesStatus, + userSelectedNotesForDeletion, } from '../store/notes.slice'; import { useAppToasts } from '../../common/hooks/use_app_toasts'; @@ -42,7 +42,8 @@ export interface DeleteNoteButtonIconProps { } /** - * Renders a button to delete a note + * Renders a button to delete a note. + * This button works in combination with the DeleteConfirmModal. */ export const DeleteNoteButtonIcon = memo(({ note, index }: DeleteNoteButtonIconProps) => { const dispatch = useDispatch(); @@ -54,8 +55,8 @@ export const DeleteNoteButtonIcon = memo(({ note, index }: DeleteNoteButtonIconP const deleteNoteFc = useCallback( (noteId: string) => { + dispatch(userSelectedNotesForDeletion(noteId)); setDeletingNoteId(noteId); - dispatch(deleteNotes({ ids: [noteId] })); }, [dispatch] ); diff --git a/x-pack/plugins/security_solution/public/notes/components/note_content.test.tsx b/x-pack/plugins/security_solution/public/notes/components/note_content.test.tsx new file mode 100644 index 0000000000000..6cc9d33d886b7 --- /dev/null +++ b/x-pack/plugins/security_solution/public/notes/components/note_content.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 { render } from '@testing-library/react'; +import React from 'react'; +import { NoteContent } from './note_content'; +import { NOTE_CONTENT_BUTTON_TEST_ID, NOTE_CONTENT_POPOVER_TEST_ID } from './test_ids'; + +const note = 'note-text'; + +describe('NoteContent', () => { + it('should render a note and the popover', () => { + const { getByTestId, getByText } = render(); + + const button = getByTestId(NOTE_CONTENT_BUTTON_TEST_ID); + + expect(button).toBeInTheDocument(); + expect(getByText(note)).toBeInTheDocument(); + + button.click(); + + expect(getByTestId(NOTE_CONTENT_POPOVER_TEST_ID)).toBeInTheDocument(); + }); +}); diff --git a/x-pack/plugins/security_solution/public/notes/components/note_content.tsx b/x-pack/plugins/security_solution/public/notes/components/note_content.tsx new file mode 100644 index 0000000000000..ba8710e85c215 --- /dev/null +++ b/x-pack/plugins/security_solution/public/notes/components/note_content.tsx @@ -0,0 +1,73 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React, { memo, useCallback, useMemo, useState } from 'react'; +import { EuiButtonEmpty, EuiMarkdownFormat, EuiPopover, useEuiTheme } from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; +import { css } from '@emotion/react'; +import { NOTE_CONTENT_BUTTON_TEST_ID, NOTE_CONTENT_POPOVER_TEST_ID } from './test_ids'; + +const OPEN_POPOVER = i18n.translate('xpack.securitySolution.notes.expandRow.buttonLabel', { + defaultMessage: 'Expand', +}); + +export interface NoteContentProps { + /** + * The note content to display + */ + note: string; +} + +/** + * Renders the note content to be displayed in the notes management table. + * The content is truncated with an expand button to show the full content within the row. + */ +export const NoteContent = memo(({ note }: NoteContentProps) => { + const { euiTheme } = useEuiTheme(); + + const [isPopoverOpen, setIsPopoverOpen] = useState(false); + + const togglePopover = useCallback(() => setIsPopoverOpen((value) => !value), []); + const closePopover = useCallback(() => setIsPopoverOpen(false), []); + + const button = useMemo( + () => ( + + {note} + + ), + [euiTheme.size.l, note, togglePopover] + ); + + return ( + + + {note} + + + ); +}); + +NoteContent.displayName = 'NoteContent'; diff --git a/x-pack/plugins/security_solution/public/notes/components/notes_list.tsx b/x-pack/plugins/security_solution/public/notes/components/notes_list.tsx index 47dcf89b06452..344935413731e 100644 --- a/x-pack/plugins/security_solution/public/notes/components/notes_list.tsx +++ b/x-pack/plugins/security_solution/public/notes/components/notes_list.tsx @@ -10,6 +10,7 @@ import { EuiAvatar, EuiComment, EuiCommentList, EuiLoadingElastic } from '@elast import { useSelector } from 'react-redux'; import { FormattedRelative } from '@kbn/i18n-react'; import { i18n } from '@kbn/i18n'; +import { DeleteConfirmModal } from './delete_confirm_modal'; import { OpenFlyoutButtonIcon } from './open_flyout_button'; import { OpenTimelineButtonIcon } from './open_timeline_button'; import { DeleteNoteButtonIcon } from './delete_note_button'; @@ -17,7 +18,11 @@ import { MarkdownRenderer } from '../../common/components/markdown_editor'; import { ADD_NOTE_LOADING_TEST_ID, NOTE_AVATAR_TEST_ID, NOTES_COMMENT_TEST_ID } from './test_ids'; import type { State } from '../../common/store'; import type { Note } from '../../../common/api/timeline'; -import { ReqStatus, selectCreateNoteStatus } from '../store/notes.slice'; +import { + ReqStatus, + selectCreateNoteStatus, + selectNotesTablePendingDeleteIds, +} from '../store/notes.slice'; import { useUserPrivileges } from '../../common/components/user_privileges'; export const ADDED_A_NOTE = i18n.translate('xpack.securitySolution.notes.addedANoteLabel', { @@ -59,41 +64,51 @@ export const NotesList = memo(({ notes, options }: NotesListProps) => { const createStatus = useSelector((state: State) => selectCreateNoteStatus(state)); + const pendingDeleteIds = useSelector(selectNotesTablePendingDeleteIds); + const isDeleteModalVisible = pendingDeleteIds.length > 0; + return ( - - {notes.map((note, index) => ( - {note.created && }} - event={ADDED_A_NOTE} - actions={ - <> - {note.eventId && !options?.hideFlyoutIcon && ( - - )} - {note.timelineId && note.timelineId.length > 0 && !options?.hideTimelineIcon && ( - - )} - {canDeleteNotes && } - - } - timelineAvatar={ - - } - > - {note.note || ''} - - ))} - {createStatus === ReqStatus.Loading && ( - - )} - + <> + + {notes.map((note, index) => ( + {note.created && }} + event={ADDED_A_NOTE} + actions={ + <> + {note.eventId && !options?.hideFlyoutIcon && ( + + )} + {note.timelineId && note.timelineId.length > 0 && !options?.hideTimelineIcon && ( + + )} + {canDeleteNotes && } + + } + timelineAvatar={ + + } + > + {note.note || ''} + + ))} + {createStatus === ReqStatus.Loading && ( + + )} + + {isDeleteModalVisible && } + ); }); diff --git a/x-pack/plugins/security_solution/public/notes/components/open_event_in_timeline.tsx b/x-pack/plugins/security_solution/public/notes/components/open_event_in_timeline.tsx deleted file mode 100644 index 43f039836ccad..0000000000000 --- a/x-pack/plugins/security_solution/public/notes/components/open_event_in_timeline.tsx +++ /dev/null @@ -1,24 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ -import React, { memo } from 'react'; -import { EuiLink } from '@elastic/eui'; -import type { EcsSecurityExtension as Ecs } from '@kbn/securitysolution-ecs'; -import { useInvestigateInTimeline } from '../../detections/components/alerts_table/timeline_actions/use_investigate_in_timeline'; -import * as i18n from './translations'; - -export const OpenEventInTimeline: React.FC<{ eventId?: string | null }> = memo(({ eventId }) => { - const ecsRowData = { event: { id: [eventId] }, _id: eventId } as Ecs; - const { investigateInTimelineAlertClick } = useInvestigateInTimeline({ ecsRowData }); - - return ( - - {i18n.VIEW_EVENT_IN_TIMELINE} - - ); -}); - -OpenEventInTimeline.displayName = 'OpenEventInTimeline'; diff --git a/x-pack/plugins/security_solution/public/notes/components/open_flyout_button.test.tsx b/x-pack/plugins/security_solution/public/notes/components/open_flyout_button.test.tsx index eed5e5bcbd5da..c22a0ebff3fce 100644 --- a/x-pack/plugins/security_solution/public/notes/components/open_flyout_button.test.tsx +++ b/x-pack/plugins/security_solution/public/notes/components/open_flyout_button.test.tsx @@ -13,6 +13,7 @@ import { OpenFlyoutButtonIcon } from './open_flyout_button'; import { useExpandableFlyoutApi } from '@kbn/expandable-flyout'; import { DocumentDetailsRightPanelKey } from '../../flyout/document_details/shared/constants/panel_keys'; import { useSourcererDataView } from '../../sourcerer/containers'; +import { TableId } from '@kbn/securitysolution-data-table'; jest.mock('@kbn/expandable-flyout'); jest.mock('../../sourcerer/containers'); @@ -27,7 +28,11 @@ describe('OpenFlyoutButtonIcon', () => { const { getByTestId } = render( - + ); @@ -41,7 +46,11 @@ describe('OpenFlyoutButtonIcon', () => { const { getByTestId } = render( - + ); @@ -54,7 +63,7 @@ describe('OpenFlyoutButtonIcon', () => { params: { id: mockEventId, indexName: 'test1,test2', - scopeId: mockTimelineId, + scopeId: TableId.alertsOnAlertsPage, }, }, }); diff --git a/x-pack/plugins/security_solution/public/notes/components/open_flyout_button.tsx b/x-pack/plugins/security_solution/public/notes/components/open_flyout_button.tsx index 0c541cc95740c..34ae9405fdf86 100644 --- a/x-pack/plugins/security_solution/public/notes/components/open_flyout_button.tsx +++ b/x-pack/plugins/security_solution/public/notes/components/open_flyout_button.tsx @@ -6,9 +6,11 @@ */ import React, { memo, useCallback } from 'react'; +import type { IconType } from '@elastic/eui'; import { EuiButtonIcon } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import { useExpandableFlyoutApi } from '@kbn/expandable-flyout'; +import { TableId } from '@kbn/securitysolution-data-table'; import { OPEN_FLYOUT_BUTTON_TEST_ID } from './test_ids'; import { useSourcererDataView } from '../../sourcerer/containers'; import { SourcererScopeName } from '../../sourcerer/store/model'; @@ -31,44 +33,51 @@ export interface OpenFlyoutButtonIconProps { * Id of the timeline to pass to the flyout for scope */ timelineId: string; + /** + * Icon type to render in the button + */ + iconType: IconType; } /** - * Renders a button to open the alert and event details flyout + * Renders a button to open the alert and event details flyout. + * This component is meant to be used in timeline and the notes management page, where the cell actions are more basic (no filter in/out). */ -export const OpenFlyoutButtonIcon = memo(({ eventId, timelineId }: OpenFlyoutButtonIconProps) => { - const { selectedPatterns } = useSourcererDataView(SourcererScopeName.timeline); +export const OpenFlyoutButtonIcon = memo( + ({ eventId, timelineId, iconType }: OpenFlyoutButtonIconProps) => { + const { selectedPatterns } = useSourcererDataView(SourcererScopeName.timeline); - const { telemetry } = useKibana().services; - const { openFlyout } = useExpandableFlyoutApi(); + const { telemetry } = useKibana().services; + const { openFlyout } = useExpandableFlyoutApi(); - const handleClick = useCallback(() => { - openFlyout({ - right: { - id: DocumentDetailsRightPanelKey, - params: { - id: eventId, - indexName: selectedPatterns.join(','), - scopeId: timelineId, + const handleClick = useCallback(() => { + openFlyout({ + right: { + id: DocumentDetailsRightPanelKey, + params: { + id: eventId, + indexName: selectedPatterns.join(','), + scopeId: TableId.alertsOnAlertsPage, // TODO we should update the flyout's code to separate scopeId and preview + }, }, - }, - }); - telemetry.reportDetailsFlyoutOpened({ - location: timelineId, - panel: 'right', - }); - }, [eventId, openFlyout, selectedPatterns, telemetry, timelineId]); + }); + telemetry.reportDetailsFlyoutOpened({ + location: timelineId, + panel: 'right', + }); + }, [eventId, openFlyout, selectedPatterns, telemetry, timelineId]); - return ( - - ); -}); + return ( + + ); + } +); OpenFlyoutButtonIcon.displayName = 'OpenFlyoutButtonIcon'; diff --git a/x-pack/plugins/security_solution/public/notes/components/open_timeline_button.tsx b/x-pack/plugins/security_solution/public/notes/components/open_timeline_button.tsx index 531983429acd1..b44ffd55a767a 100644 --- a/x-pack/plugins/security_solution/public/notes/components/open_timeline_button.tsx +++ b/x-pack/plugins/security_solution/public/notes/components/open_timeline_button.tsx @@ -7,11 +7,16 @@ import React, { memo, useCallback } from 'react'; import { EuiButtonIcon } from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; import { useIsExperimentalFeatureEnabled } from '../../common/hooks/use_experimental_features'; import { useQueryTimelineById } from '../../timelines/components/open_timeline/helpers'; import { OPEN_TIMELINE_BUTTON_TEST_ID } from './test_ids'; import type { Note } from '../../../common/api/timeline'; +const OPEN_TIMELINE = i18n.translate('xpack.securitySolution.notes.management.openTimelineButton', { + defaultMessage: 'Open saved timeline', +}); + export interface OpenTimelineButtonIconProps { /** * The note that contains the id of the timeline to open @@ -20,7 +25,7 @@ export interface OpenTimelineButtonIconProps { /** * The index of the note in the list of notes (used to have unique data-test-subj) */ - index: number; + index?: number; } /** @@ -47,10 +52,10 @@ export const OpenTimelineButtonIcon = memo(({ note, index }: OpenTimelineButtonI return ( openTimeline(note)} /> ); diff --git a/x-pack/plugins/security_solution/public/notes/components/test_ids.ts b/x-pack/plugins/security_solution/public/notes/components/test_ids.ts index 6c63a43f365ac..ac4eeb1948748 100644 --- a/x-pack/plugins/security_solution/public/notes/components/test_ids.ts +++ b/x-pack/plugins/security_solution/public/notes/components/test_ids.ts @@ -17,3 +17,5 @@ export const DELETE_NOTE_BUTTON_TEST_ID = `${PREFIX}DeleteNotesButton` as const; export const OPEN_TIMELINE_BUTTON_TEST_ID = `${PREFIX}OpenTimelineButton` as const; export const OPEN_FLYOUT_BUTTON_TEST_ID = `${PREFIX}OpenFlyoutButton` as const; export const TIMELINE_DESCRIPTION_COMMENT_TEST_ID = `${PREFIX}TimelineDescriptionComment` as const; +export const NOTE_CONTENT_BUTTON_TEST_ID = `${PREFIX}NoteContentButton` as const; +export const NOTE_CONTENT_POPOVER_TEST_ID = `${PREFIX}NoteContentPopover` as const; diff --git a/x-pack/plugins/security_solution/public/notes/components/translations.ts b/x-pack/plugins/security_solution/public/notes/components/translations.ts deleted file mode 100644 index 8d7a5b4262815..0000000000000 --- a/x-pack/plugins/security_solution/public/notes/components/translations.ts +++ /dev/null @@ -1,58 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license 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 BATCH_ACTIONS = i18n.translate( - 'xpack.securitySolution.notes.management.batchActionsTitle', - { - defaultMessage: 'Bulk actions', - } -); - -export const DELETE = i18n.translate('xpack.securitySolution.notes.management.deleteAction', { - defaultMessage: 'Delete', -}); - -export const DELETE_NOTES_MODAL_TITLE = i18n.translate( - 'xpack.securitySolution.notes.management.deleteNotesModalTitle', - { - defaultMessage: 'Delete notes?', - } -); - -export const DELETE_NOTES_CONFIRM = (selectedNotes: number) => - i18n.translate('xpack.securitySolution.notes.management.deleteNotesConfirm', { - values: { selectedNotes }, - defaultMessage: - 'Are you sure you want to delete {selectedNotes} {selectedNotes, plural, one {note} other {notes}}?', - }); - -export const DELETE_NOTES_CANCEL = i18n.translate( - 'xpack.securitySolution.notes.management.deleteNotesCancel', - { - defaultMessage: 'Cancel', - } -); - -export const DELETE_SELECTED = i18n.translate( - 'xpack.securitySolution.notes.management.deleteSelected', - { - defaultMessage: 'Delete selected notes', - } -); - -export const REFRESH = i18n.translate('xpack.securitySolution.notes.management.refresh', { - defaultMessage: 'Refresh', -}); - -export const VIEW_EVENT_IN_TIMELINE = i18n.translate( - 'xpack.securitySolution.notes.management.viewEventInTimeline', - { - defaultMessage: 'View event in timeline', - } -); diff --git a/x-pack/plugins/security_solution/public/notes/components/utility_bar.tsx b/x-pack/plugins/security_solution/public/notes/components/utility_bar.tsx index 0c09f6393f668..f0a337cb6c217 100644 --- a/x-pack/plugins/security_solution/public/notes/components/utility_bar.tsx +++ b/x-pack/plugins/security_solution/public/notes/components/utility_bar.tsx @@ -4,9 +4,11 @@ * 2.0; you may not use this file except in compliance with the Elastic License * 2.0. */ + import React, { useMemo, useCallback } from 'react'; import { useDispatch, useSelector } from 'react-redux'; import { EuiContextMenuItem } from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; import { UtilityBarGroup, UtilityBarText, @@ -22,8 +24,28 @@ import { selectNotesTableSearch, userSelectedBulkDelete, } from '..'; -import * as i18n from './translations'; +export const BATCH_ACTIONS = i18n.translate( + 'xpack.securitySolution.notes.management.batchActionsTitle', + { + defaultMessage: 'Bulk actions', + } +); + +export const DELETE_SELECTED = i18n.translate( + 'xpack.securitySolution.notes.management.deleteSelected', + { + defaultMessage: 'Delete selected notes', + } +); + +export const REFRESH = i18n.translate('xpack.securitySolution.notes.management.refresh', { + defaultMessage: 'Refresh', +}); + +/** + * Renders the utility bar for the notes management page + */ export const NotesUtilityBar = React.memo(() => { const dispatch = useDispatch(); const pagination = useSelector(selectNotesPagination); @@ -49,7 +71,7 @@ export const NotesUtilityBar = React.memo(() => { icon="trash" key="DeleteItemKey" > - {i18n.DELETE_SELECTED} + {DELETE_SELECTED} ); }, [deleteSelectedNotes, selectedItems.length]); @@ -83,9 +105,7 @@ export const NotesUtilityBar = React.memo(() => { iconType="arrowDown" popoverContent={BulkActionPopoverContent} > - - {i18n.BATCH_ACTIONS} - + {BATCH_ACTIONS} { iconType="refresh" onClick={refresh} > - {i18n.REFRESH} + {REFRESH} diff --git a/x-pack/plugins/security_solution/public/notes/hooks/use_fetch_notes.ts b/x-pack/plugins/security_solution/public/notes/hooks/use_fetch_notes.ts index c9f64bc382454..2cf599e76bcc9 100644 --- a/x-pack/plugins/security_solution/public/notes/hooks/use_fetch_notes.ts +++ b/x-pack/plugins/security_solution/public/notes/hooks/use_fetch_notes.ts @@ -4,12 +4,23 @@ * 2.0; you may not use this file except in compliance with the Elastic License * 2.0. */ + import { useCallback } from 'react'; import { useDispatch } from 'react-redux'; import { useIsExperimentalFeatureEnabled } from '../../common/hooks/use_experimental_features'; -import { fetchNotesByDocumentIds } from '..'; +import { fetchNotesByDocumentIds } from '../store/notes.slice'; + +export interface UseFetchNotesResult { + /** + * Function to fetch the notes for an array of documents + */ + onLoad: (events: Array>) => void; +} -export const useFetchNotes = () => { +/** + * Hook that returns a function to fetch the notes for an array of documents + */ +export const useFetchNotes = (): UseFetchNotesResult => { const dispatch = useDispatch(); const securitySolutionNotesEnabled = useIsExperimentalFeatureEnabled( 'securitySolutionNotesEnabled' diff --git a/x-pack/plugins/security_solution/public/notes/pages/note_management_page.tsx b/x-pack/plugins/security_solution/public/notes/pages/note_management_page.tsx index ddfed3fbb6287..2b7f0f690532c 100644 --- a/x-pack/plugins/security_solution/public/notes/pages/note_management_page.tsx +++ b/x-pack/plugins/security_solution/public/notes/pages/note_management_page.tsx @@ -6,11 +6,18 @@ */ import React, { useCallback, useMemo, useEffect } from 'react'; -import type { DefaultItemAction, EuiBasicTableColumn } from '@elastic/eui'; -import { EuiBasicTable, EuiEmptyPrompt, EuiLink, EuiSpacer } from '@elastic/eui'; +import type { EuiBasicTableColumn } from '@elastic/eui'; +import { + EuiAvatar, + EuiBasicTable, + EuiEmptyPrompt, + EuiFlexGroup, + EuiFlexItem, + EuiSpacer, +} from '@elastic/eui'; import { useDispatch, useSelector } from 'react-redux'; -import { useIsExperimentalFeatureEnabled } from '../../common/hooks/use_experimental_features'; -import { useQueryTimelineById } from '../../timelines/components/open_timeline/helpers'; +import { css } from '@emotion/react'; +import { DeleteNoteButtonIcon } from '../components/delete_note_button'; import { Title } from '../../common/components/header_page/title'; // TODO unify this type from the api with the one in public/common/lib/note import type { Note } from '../../../common/api/timeline'; @@ -27,7 +34,6 @@ import { selectNotesTableSearch, selectFetchNotesStatus, selectNotesTablePendingDeleteIds, - userSelectedRowForDeletion, selectFetchNotesError, ReqStatus, } from '..'; @@ -36,42 +42,69 @@ import { SearchRow } from '../components/search_row'; import { NotesUtilityBar } from '../components/utility_bar'; import { DeleteConfirmModal } from '../components/delete_confirm_modal'; import * as i18n from './translations'; -import { OpenEventInTimeline } from '../components/open_event_in_timeline'; - -const columns: ( - onOpenTimeline: (timelineId: string) => void -) => Array> = (onOpenTimeline) => { - return [ - { - field: 'created', - name: i18n.CREATED_COLUMN, - sortable: true, - render: (created: Note['created']) => , - }, - { - field: 'createdBy', - name: i18n.CREATED_BY_COLUMN, - }, - { - field: 'eventId', - name: i18n.EVENT_ID_COLUMN, - sortable: true, - render: (eventId: Note['eventId']) => , - }, - { - field: 'timelineId', - name: i18n.TIMELINE_ID_COLUMN, - render: (timelineId: Note['timelineId']) => - timelineId ? ( - onOpenTimeline(timelineId)}>{i18n.OPEN_TIMELINE} - ) : null, - }, - { - field: 'note', - name: i18n.NOTE_CONTENT_COLUMN, - }, - ]; -}; +import { OpenFlyoutButtonIcon } from '../components/open_flyout_button'; +import { OpenTimelineButtonIcon } from '../components/open_timeline_button'; +import { NoteContent } from '../components/note_content'; + +const columns: Array> = [ + { + name: i18n.ACTIONS_COLUMN, + render: (note: Note) => ( + + + {note.eventId ? ( + + ) : null} + + + <>{note.timelineId ? : null} + + + + + + ), + width: '72px', + }, + { + field: 'createdBy', + name: i18n.CREATED_BY_COLUMN, + render: (createdBy: Note['createdBy']) => , + width: '100px', + align: 'center', + }, + { + field: 'note', + name: i18n.NOTE_CONTENT_COLUMN, + render: (note: Note['note']) => <>{note && }, + }, + { + field: 'created', + name: i18n.CREATED_COLUMN, + sortable: true, + render: (created: Note['created']) => , + width: '225px', + }, +]; const pageSizeOptions = [10, 25, 50, 100]; @@ -129,13 +162,6 @@ export const NoteManagementPage = () => { [dispatch] ); - const selectRowForDeletion = useCallback( - (id: string) => { - dispatch(userSelectedRowForDeletion(id)); - }, - [dispatch] - ); - const onSelectionChange = useCallback( (selection: Note[]) => { const rowIds = selection.map((item) => item.noteId); @@ -148,39 +174,6 @@ export const NoteManagementPage = () => { return item.noteId; }, []); - const unifiedComponentsInTimelineDisabled = useIsExperimentalFeatureEnabled( - 'unifiedComponentsInTimelineDisabled' - ); - const queryTimelineById = useQueryTimelineById(); - const openTimeline = useCallback( - (timelineId: string) => - queryTimelineById({ - timelineId, - unifiedComponentsInTimelineDisabled, - }), - [queryTimelineById, unifiedComponentsInTimelineDisabled] - ); - - const columnWithActions = useMemo(() => { - const actions: Array> = [ - { - name: i18n.DELETE, - description: i18n.DELETE_SINGLE_NOTE_DESCRIPTION, - color: 'primary', - icon: 'trash', - type: 'icon', - onClick: (note: Note) => selectRowForDeletion(note.noteId), - }, - ]; - return [ - ...columns(openTimeline), - { - name: 'actions', - actions, - }, - ]; - }, [selectRowForDeletion, openTimeline]); - const currentPagination = useMemo(() => { return { pageIndex: pagination.page - 1, @@ -223,7 +216,7 @@ export const NoteManagementPage = () => { { }); }); - describe('userSelectedRowForDeletion', () => { - it('should set correct id when user selects a row', () => { - const action = { type: userSelectedRowForDeletion.type, payload: '1' }; + describe('userSelectedNotesForDeletion', () => { + it('should set correct id when user selects a note to delete', () => { + const action = { type: userSelectedNotesForDeletion.type, payload: '1' }; expect(notesReducer(initalEmptyState, action)).toEqual({ ...initalEmptyState, diff --git a/x-pack/plugins/security_solution/public/notes/store/notes.slice.ts b/x-pack/plugins/security_solution/public/notes/store/notes.slice.ts index 6732f9491676e..2d24ab838ee06 100644 --- a/x-pack/plugins/security_solution/public/notes/store/notes.slice.ts +++ b/x-pack/plugins/security_solution/public/notes/store/notes.slice.ts @@ -193,7 +193,7 @@ const notesSlice = createSlice({ userClosedDeleteModal: (state) => { state.pendingDeleteIds = []; }, - userSelectedRowForDeletion: (state, action: { payload: string }) => { + userSelectedNotesForDeletion: (state, action: { payload: string }) => { state.pendingDeleteIds = [action.payload]; }, userSelectedBulkDelete: (state) => { @@ -391,6 +391,6 @@ export const { userSearchedNotes, userSelectedRow, userClosedDeleteModal, - userSelectedRowForDeletion, + userSelectedNotesForDeletion, userSelectedBulkDelete, } = notesSlice.actions; diff --git a/x-pack/plugins/security_solution/scripts/run_cypress/parallel.ts b/x-pack/plugins/security_solution/scripts/run_cypress/parallel.ts index 85cbbbe0eb7fd..0f93e4fceb10c 100644 --- a/x-pack/plugins/security_solution/scripts/run_cypress/parallel.ts +++ b/x-pack/plugins/security_solution/scripts/run_cypress/parallel.ts @@ -89,8 +89,10 @@ ${JSON.stringify(cypressConfigFile, null, 2)} const specConfig = cypressConfigFile.e2e.specPattern; const specArg = argv.spec; const specPattern = specArg ?? specConfig; + const excludeSpecPattern = cypressConfigFile.e2e.excludeSpecPattern; log.info('Config spec pattern:', specConfig); + log.info('Exclude spec pattern:', excludeSpecPattern); log.info('Arguments spec pattern:', specArg); log.info('Resulting spec pattern:', specPattern); @@ -123,7 +125,14 @@ ${JSON.stringify(cypressConfigFile, null, 2)} const concreteFilePaths = isGrepReturnedFilePaths ? grepSpecPattern // use the returned concrete file paths - : globby.sync(specPattern); // convert the glob pattern to concrete file paths + : globby.sync( + specPattern, + excludeSpecPattern + ? { + ignore: excludeSpecPattern, + } + : undefined + ); // convert the glob pattern to concrete file paths let files = retrieveIntegrations(concreteFilePaths); diff --git a/x-pack/plugins/security_solution/scripts/telemetry/saved_objects/security_solution_ebt_kibana_browser.ndjson b/x-pack/plugins/security_solution/scripts/telemetry/saved_objects/security_solution_ebt_kibana_browser.ndjson index be4eb8f1e7785..f0df277ff5223 100644 --- a/x-pack/plugins/security_solution/scripts/telemetry/saved_objects/security_solution_ebt_kibana_browser.ndjson +++ b/x-pack/plugins/security_solution/scripts/telemetry/saved_objects/security_solution_ebt_kibana_browser.ndjson @@ -1,2 +1,2 @@ -{"attributes":{"allowHidden":false,"fieldAttrs":"{\"properties.groupingId\":{\"count\":1},\"properties.target\":{\"count\":1},\"properties.groupName\":{\"count\":2},\"properties.metadata.telemetry.component\":{\"count\":2},\"properties.unallowedMappingFields\":{\"count\":2},\"properties.unallowedValueFields\":{\"count\":1},\"context.labels.serverless\":{\"count\":4},\"properties.tableId\":{\"count\":1},\"properties.groupNumber\":{\"count\":1},\"properties.groupByField\":{\"count\":4},\"properties.status\":{\"count\":1},\"properties.conversationId\":{\"count\":17},\"properties.invokedBy\":{\"count\":7},\"properties.role\":{\"count\":3},\"properties.isEnabledKnowledgeBase\":{\"count\":1},\"properties.isEnabledRAGAlerts\":{\"count\":1},\"properties.promptTitle\":{\"count\":3},\"properties.fieldName\":{\"count\":1},\"properties.actionId\":{\"count\":1},\"properties.displayName\":{\"count\":1},\"properties.batchId\":{\"count\":8},\"properties.indexId\":{\"count\":1},\"properties.indexName\":{\"count\":2},\"properties.numberOfIndices\":{\"count\":1},\"properties.timeConsumedMs\":{\"count\":1},\"properties.ecsVersion\":{\"count\":1},\"properties.errorCount\":{\"count\":1},\"properties.numberOfIncompatibleFields\":{\"count\":1},\"properties.numberOfDocuments\":{\"count\":1},\"properties.sizeInBytes\":{\"count\":4},\"properties.isCheckAll\":{\"count\":5},\"properties.ilmPhase\":{\"count\":2},\"properties.title\":{\"count\":1},\"properties.location\":{\"count\":1},\"context.applicationId\":{\"count\":6},\"context.cloudId\":{\"count\":6},\"context.cluster_name\":{\"count\":13},\"context.cluster_uuid\":{\"count\":28},\"context.cluster_version\":{\"count\":2},\"context.license_type\":{\"count\":1},\"context.page\":{\"count\":8},\"context.pageName\":{\"count\":6},\"context.page_title\":{\"count\":1},\"context.page_url\":{\"count\":1},\"context.session_id\":{\"count\":2},\"event_type\":{\"count\":36},\"properties\":{\"count\":8},\"properties.pattern\":{\"count\":2},\"peoperties.indexName\":{\"count\":1},\"properties.stepId\":{},\"properties.trigger\":{},\"properties.stepLinkId\":{},\"properties.originStepId\":{},\"properties.durationMs\":{},\"properties.isOpen\":{},\"properties.actionTypeId\":{},\"properties.model\":{},\"properties.provider\":{},\"properties.assistantStreamingEnabled\":{},\"properties.alertsContextCount\":{},\"properties.alertsCount\":{},\"properties.configuredAlertsCount\":{},\"properties.entity\":{},\"properties.selectedSeverity\":{},\"properties.file.size\":{},\"properties.processing.startTime\":{},\"properties.processing.endTime\":{},\"properties.processing.tookMs\":{},\"properties.stats.validLines\":{},\"properties.stats.invalidLines\":{},\"properties.stats.totalLines\":{},\"properties.valid\":{},\"properties.errorCode\":{},\"properties.action\":{},\"properties.quantity\":{},\"properties.jobId\":{},\"properties.isElasticJob\":{},\"properties.moduleId\":{},\"properties.errorMessage\":{},\"properties.count\":{},\"properties.numberOfIndicesChecked\":{},\"properties.numberOfSameFamily\":{},\"properties.numberOfFields\":{},\"properties.numberOfEcsFields\":{},\"properties.numberOfCustomFields\":{},\"properties.panel\":{},\"properties.tabId\":{}}","fieldFormatMap":"{}","fields":"[]","name":"security-solution-ebt-kibana-browser","runtimeFieldMap":"{\"properties.groupingId\":{\"type\":\"keyword\"},\"properties.target\":{\"type\":\"keyword\"},\"property.stackByField\":{\"type\":\"keyword\"},\"properties.groupName\":{\"type\":\"keyword\"},\"context.prebuiltRulesPackageVersion\":{\"type\":\"keyword\"},\"properties.metadata.telemetry.component\":{\"type\":\"keyword\"},\"properties.unallowedMappingFields\":{\"type\":\"keyword\"},\"properties.unallowedValueFields\":{\"type\":\"keyword\"},\"context.labels.serverless\":{\"type\":\"keyword\"},\"properties.resourceAccessed\":{\"type\":\"keyword\"},\"properties.resultCount\":{\"type\":\"long\"},\"properties.responseTime\":{\"type\":\"long\"},\"day_of_week\":{\"type\":\"keyword\",\"script\":{\"source\":\"emit(doc['timestamp'].value.getDayOfWeek().getDisplayName(TextStyle.FULL, Locale.getDefault()))\"}},\"properties.isOpen\":{\"type\":\"boolean\"},\"properties.tableId\":{\"type\":\"keyword\"},\"properties.groupNumber\":{\"type\":\"long\"},\"properties.groupByField\":{\"type\":\"keyword\"},\"properties.status\":{\"type\":\"keyword\"},\"properties.conversationId\":{\"type\":\"keyword\"},\"properties.invokedBy\":{\"type\":\"keyword\"},\"properties.role\":{\"type\":\"keyword\"},\"properties.isEnabledKnowledgeBase\":{\"type\":\"boolean\"},\"properties.isEnabledRAGAlerts\":{\"type\":\"boolean\"},\"properties.actionTypeId\":{\"type\":\"keyword\"},\"properties.model\":{\"type\":\"keyword\"},\"properties.provider\":{\"type\":\"keyword\"},\"properties.promptTitle\":{\"type\":\"keyword\"},\"properties.assistantStreamingEnabled\":{\"type\":\"boolean\"},\"properties.durationMs\":{\"type\":\"long\"},\"properties.alertsContextCount\":{\"type\":\"long\"},\"properties.alertsCount\":{\"type\":\"long\"},\"properties.configuredAlertsCount\":{\"type\":\"long\"},\"properties.entity\":{\"type\":\"keyword\"},\"properties.selectedSeverity\":{\"type\":\"keyword\"},\"properties.file.size\":{\"type\":\"long\"},\"properties.processing.startTime\":{\"type\":\"date\"},\"properties.processing.endTime\":{\"type\":\"date\"},\"properties.processing.tookMs\":{\"type\":\"long\"},\"properties.stats.validLines\":{\"type\":\"long\"},\"properties.stats.invalidLines\":{\"type\":\"long\"},\"properties.stats.totalLines\":{\"type\":\"long\"},\"properties.valid\":{\"type\":\"boolean\"},\"properties.errorCode\":{\"type\":\"keyword\"},\"properties.action\":{\"type\":\"keyword\"},\"properties.quantity\":{\"type\":\"long\"},\"properties.jobId\":{\"type\":\"keyword\"},\"properties.isElasticJob\":{\"type\":\"boolean\"},\"properties.moduleId\":{\"type\":\"keyword\"},\"properties.errorMessage\":{\"type\":\"keyword\"},\"properties.fieldName\":{\"type\":\"keyword\"},\"properties.actionId\":{\"type\":\"keyword\"},\"properties.displayName\":{\"type\":\"keyword\"},\"properties.count\":{\"type\":\"long\"},\"properties.batchId\":{\"type\":\"keyword\"},\"properties.indexId\":{\"type\":\"keyword\"},\"properties.indexName\":{\"type\":\"keyword\"},\"properties.numberOfIndices\":{\"type\":\"long\"},\"properties.numberOfIndicesChecked\":{\"type\":\"long\"},\"properties.numberOfSameFamily\":{\"type\":\"long\"},\"properties.timeConsumedMs\":{\"type\":\"long\"},\"properties.ecsVersion\":{\"type\":\"keyword\"},\"properties.errorCount\":{\"type\":\"long\"},\"properties.numberOfFields\":{\"type\":\"long\"},\"properties.numberOfIncompatibleFields\":{\"type\":\"long\"},\"properties.numberOfEcsFields\":{\"type\":\"long\"},\"properties.numberOfCustomFields\":{\"type\":\"long\"},\"properties.numberOfDocuments\":{\"type\":\"long\"},\"properties.sizeInBytes\":{\"type\":\"long\"},\"properties.isCheckAll\":{\"type\":\"boolean\"},\"properties.ilmPhase\":{\"type\":\"keyword\"},\"properties.title\":{\"type\":\"keyword\"},\"properties.location\":{\"type\":\"keyword\"},\"properties.panel\":{\"type\":\"keyword\"},\"properties.tabId\":{\"type\":\"keyword\"},\"properties.stepId\":{\"type\":\"keyword\"},\"properties.trigger\":{\"type\":\"keyword\"},\"properties.originStepId\":{\"type\":\"keyword\"},\"properties.stepLinkId\":{\"type\":\"keyword\"}}","sourceFilters":"[]","timeFieldName":"timestamp","title":"ebt-kibana-browser","typeMeta":"{}"},"coreMigrationVersion":"8.8.0","created_at":"2024-05-30T16:12:33.003Z","id":"security-solution-ebt-kibana-browser","managed":false,"references":[],"type":"index-pattern","typeMigrationVersion":"8.0.0","updated_at":"2024-05-30T16:52:03.990Z","version":"WzMwNTU0LDVd"} -{"excludedObjects":[],"excludedObjectsCount":0,"exportedCount":1,"missingRefCount":0,"missingReferences":[]} +{"attributes":{"allowHidden":false,"fieldAttrs":"{\"properties.groupingId\":{\"count\":1},\"properties.target\":{\"count\":1},\"properties.groupName\":{\"count\":2},\"properties.metadata.telemetry.component\":{\"count\":2},\"properties.unallowedMappingFields\":{\"count\":2},\"properties.unallowedValueFields\":{\"count\":1},\"context.labels.serverless\":{\"count\":4},\"properties.isEnabledRAGAlerts\":{\"count\":1},\"properties.tableId\":{\"count\":1},\"properties.groupNumber\":{\"count\":1},\"properties.groupByField\":{\"count\":4},\"properties.status\":{\"count\":1},\"properties.conversationId\":{\"count\":17},\"properties.invokedBy\":{\"count\":7},\"properties.role\":{\"count\":3},\"properties.isEnabledKnowledgeBase\":{\"count\":1},\"properties.promptTitle\":{\"count\":3},\"properties.fieldName\":{\"count\":1},\"properties.actionId\":{\"count\":1},\"properties.displayName\":{\"count\":1},\"properties.batchId\":{\"count\":8},\"properties.indexId\":{\"count\":1},\"properties.indexName\":{\"count\":2},\"properties.numberOfIndices\":{\"count\":1},\"properties.timeConsumedMs\":{\"count\":1},\"properties.ecsVersion\":{\"count\":1},\"properties.errorCount\":{\"count\":1},\"properties.numberOfIncompatibleFields\":{\"count\":1},\"properties.numberOfDocuments\":{\"count\":1},\"properties.sizeInBytes\":{\"count\":4},\"properties.isCheckAll\":{\"count\":5},\"properties.ilmPhase\":{\"count\":2},\"properties.title\":{\"count\":1},\"properties.location\":{\"count\":1},\"context.applicationId\":{\"count\":6},\"context.cloudId\":{\"count\":6},\"context.cluster_name\":{\"count\":13},\"context.cluster_uuid\":{\"count\":28},\"context.cluster_version\":{\"count\":2},\"context.license_type\":{\"count\":1},\"context.page\":{\"count\":8},\"context.pageName\":{\"count\":6},\"context.page_title\":{\"count\":1},\"context.page_url\":{\"count\":1},\"context.session_id\":{\"count\":2},\"event_type\":{\"count\":36},\"properties\":{\"count\":8},\"properties.pattern\":{\"count\":2},\"peoperties.indexName\":{\"count\":1},\"properties.stepId\":{},\"properties.trigger\":{},\"properties.stepLinkId\":{},\"properties.originStepId\":{},\"properties.durationMs\":{},\"properties.isOpen\":{},\"properties.actionTypeId\":{},\"properties.model\":{},\"properties.provider\":{},\"properties.assistantStreamingEnabled\":{},\"properties.alertsContextCount\":{},\"properties.alertsCount\":{},\"properties.configuredAlertsCount\":{},\"properties.entity\":{},\"properties.selectedSeverity\":{},\"properties.file.size\":{},\"properties.processing.startTime\":{},\"properties.processing.endTime\":{},\"properties.processing.tookMs\":{},\"properties.stats.validLines\":{},\"properties.stats.invalidLines\":{},\"properties.stats.totalLines\":{},\"properties.valid\":{},\"properties.errorCode\":{},\"properties.action\":{},\"properties.quantity\":{},\"properties.jobId\":{},\"properties.isElasticJob\":{},\"properties.moduleId\":{},\"properties.errorMessage\":{},\"properties.count\":{},\"properties.numberOfIndicesChecked\":{},\"properties.numberOfSameFamily\":{},\"properties.numberOfFields\":{},\"properties.numberOfEcsFields\":{},\"properties.numberOfCustomFields\":{},\"properties.panel\":{},\"properties.tabId\":{},\"properties.totalTasks\":{},\"properties.completedTasks\":{},\"properties.errorTasks\":{},\"properties.rangeInMs\":{},\"properties.type\":{},\"properties.runType\":{},\"properties.isVisible\":{},\"properties.alertsCountUpdated\":{},\"properties.rulesCount\":{},\"properties.isRelatedToATimeline\":{},\"propeties.loggedRequestsEnabled\":{},\"properties.ruleType\":{},\"properties.loggedRequestsEnabled\":{}}","fieldFormatMap":"{}","fields":"[]","name":"security-solution-ebt-kibana-browser","runtimeFieldMap":"{\"properties.groupingId\":{\"type\":\"keyword\"},\"properties.target\":{\"type\":\"keyword\"},\"property.stackByField\":{\"type\":\"keyword\"},\"properties.groupName\":{\"type\":\"keyword\"},\"context.prebuiltRulesPackageVersion\":{\"type\":\"keyword\"},\"properties.metadata.telemetry.component\":{\"type\":\"keyword\"},\"properties.unallowedMappingFields\":{\"type\":\"keyword\"},\"properties.unallowedValueFields\":{\"type\":\"keyword\"},\"context.labels.serverless\":{\"type\":\"keyword\"},\"properties.resourceAccessed\":{\"type\":\"keyword\"},\"properties.resultCount\":{\"type\":\"long\"},\"properties.responseTime\":{\"type\":\"long\"},\"day_of_week\":{\"type\":\"keyword\",\"script\":{\"source\":\"emit(doc['timestamp'].value.getDayOfWeek().getDisplayName(TextStyle.FULL, Locale.getDefault()))\"}},\"properties.isEnabledRAGAlerts\":{\"type\":\"boolean\"},\"properties.durationMs\":{\"type\":\"long\"},\"properties.alertsContextCount\":{\"type\":\"long\"},\"properties.alertsCount\":{\"type\":\"long\"},\"properties.configuredAlertsCount\":{\"type\":\"long\"},\"properties.runType\":{\"type\":\"keyword\"},\"properties.isOpen\":{\"type\":\"boolean\"},\"properties.tableId\":{\"type\":\"keyword\"},\"properties.groupNumber\":{\"type\":\"long\"},\"properties.groupByField\":{\"type\":\"keyword\"},\"properties.status\":{\"type\":\"keyword\"},\"properties.conversationId\":{\"type\":\"keyword\"},\"properties.invokedBy\":{\"type\":\"keyword\"},\"properties.role\":{\"type\":\"keyword\"},\"properties.actionTypeId\":{\"type\":\"keyword\"},\"properties.model\":{\"type\":\"keyword\"},\"properties.provider\":{\"type\":\"keyword\"},\"properties.isEnabledKnowledgeBase\":{\"type\":\"boolean\"},\"properties.promptTitle\":{\"type\":\"keyword\"},\"properties.alertsCountUpdated\":{\"type\":\"boolean\"},\"properties.assistantStreamingEnabled\":{\"type\":\"boolean\"},\"properties.entity\":{\"type\":\"keyword\"},\"properties.selectedSeverity\":{\"type\":\"keyword\"},\"properties.file.size\":{\"type\":\"long\"},\"properties.processing.startTime\":{\"type\":\"date\"},\"properties.processing.endTime\":{\"type\":\"date\"},\"properties.processing.tookMs\":{\"type\":\"long\"},\"properties.stats.validLines\":{\"type\":\"long\"},\"properties.stats.invalidLines\":{\"type\":\"long\"},\"properties.stats.totalLines\":{\"type\":\"long\"},\"properties.valid\":{\"type\":\"boolean\"},\"properties.errorCode\":{\"type\":\"keyword\"},\"properties.action\":{\"type\":\"keyword\"},\"properties.quantity\":{\"type\":\"long\"},\"properties.jobId\":{\"type\":\"keyword\"},\"properties.isElasticJob\":{\"type\":\"boolean\"},\"properties.moduleId\":{\"type\":\"keyword\"},\"properties.errorMessage\":{\"type\":\"keyword\"},\"properties.fieldName\":{\"type\":\"keyword\"},\"properties.actionId\":{\"type\":\"keyword\"},\"properties.displayName\":{\"type\":\"keyword\"},\"properties.count\":{\"type\":\"long\"},\"properties.batchId\":{\"type\":\"keyword\"},\"properties.indexId\":{\"type\":\"keyword\"},\"properties.indexName\":{\"type\":\"keyword\"},\"properties.numberOfIndices\":{\"type\":\"long\"},\"properties.numberOfIndicesChecked\":{\"type\":\"long\"},\"properties.numberOfSameFamily\":{\"type\":\"long\"},\"properties.timeConsumedMs\":{\"type\":\"long\"},\"properties.ecsVersion\":{\"type\":\"keyword\"},\"properties.errorCount\":{\"type\":\"long\"},\"properties.numberOfFields\":{\"type\":\"long\"},\"properties.numberOfIncompatibleFields\":{\"type\":\"long\"},\"properties.numberOfEcsFields\":{\"type\":\"long\"},\"properties.numberOfCustomFields\":{\"type\":\"long\"},\"properties.numberOfDocuments\":{\"type\":\"long\"},\"properties.sizeInBytes\":{\"type\":\"long\"},\"properties.isCheckAll\":{\"type\":\"boolean\"},\"properties.ilmPhase\":{\"type\":\"keyword\"},\"properties.title\":{\"type\":\"keyword\"},\"properties.location\":{\"type\":\"keyword\"},\"properties.panel\":{\"type\":\"keyword\"},\"properties.tabId\":{\"type\":\"keyword\"},\"properties.stepId\":{\"type\":\"keyword\"},\"properties.trigger\":{\"type\":\"keyword\"},\"properties.originStepId\":{\"type\":\"keyword\"},\"properties.stepLinkId\":{\"type\":\"keyword\"},\"properties.totalTasks\":{\"type\":\"long\"},\"properties.completedTasks\":{\"type\":\"long\"},\"properties.errorTasks\":{\"type\":\"long\"},\"properties.rangeInMs\":{\"type\":\"long\"},\"properties.rulesCount\":{\"type\":\"long\"},\"properties.type\":{\"type\":\"keyword\"},\"properties.isVisible\":{\"type\":\"boolean\"},\"properties.isRelatedToATimeline\":{\"type\":\"boolean\"},\"properties.ruleType\":{\"type\":\"keyword\"},\"properties.loggedRequestsEnabled\":{\"type\":\"boolean\"}}","sourceFilters":"[]","timeFieldName":"timestamp","title":"ebt-kibana-browser","typeMeta":"{}"},"coreMigrationVersion":"8.8.0","created_at":"2024-05-30T16:12:33.003Z","id":"security-solution-ebt-kibana-browser","managed":false,"references":[],"type":"index-pattern","typeMigrationVersion":"8.0.0","updated_at":"2024-10-09T14:55:41.854Z","version":"WzUyMTQ4LDld"} +{"excludedObjects":[],"excludedObjectsCount":0,"exportedCount":1,"missingRefCount":0,"missingReferences":[]} \ No newline at end of file diff --git a/x-pack/plugins/security_solution/server/assistant/tools/alert_counts/alert_counts_tool.test.ts b/x-pack/plugins/security_solution/server/assistant/tools/alert_counts/alert_counts_tool.test.ts index 752f8e472a755..814a00853927f 100644 --- a/x-pack/plugins/security_solution/server/assistant/tools/alert_counts/alert_counts_tool.test.ts +++ b/x-pack/plugins/security_solution/server/assistant/tools/alert_counts/alert_counts_tool.test.ts @@ -29,13 +29,11 @@ describe('AlertCountsTool', () => { } as unknown as KibanaRequest; const isEnabledKnowledgeBase = true; const chain = {} as unknown as RetrievalQAChain; - const modelExists = true; const logger = loggerMock.create(); const rest = { isEnabledKnowledgeBase, chain, logger, - modelExists, }; beforeEach(() => { diff --git a/x-pack/plugins/security_solution/server/assistant/tools/attack_discovery/attack_discovery_tool.test.ts b/x-pack/plugins/security_solution/server/assistant/tools/attack_discovery/attack_discovery_tool.test.ts index 5d8fb0b51739a..4d06751f57d7d 100644 --- a/x-pack/plugins/security_solution/server/assistant/tools/attack_discovery/attack_discovery_tool.test.ts +++ b/x-pack/plugins/security_solution/server/assistant/tools/attack_discovery/attack_discovery_tool.test.ts @@ -75,7 +75,6 @@ describe('AttackDiscoveryTool', () => { isEnabledKnowledgeBase: false, llm, logger, - modelExists: false, onNewReplacements: jest.fn(), size, }; diff --git a/x-pack/plugins/security_solution/server/assistant/tools/esql_language_knowledge_base/common.ts b/x-pack/plugins/security_solution/server/assistant/tools/esql/common.ts similarity index 100% rename from x-pack/plugins/security_solution/server/assistant/tools/esql_language_knowledge_base/common.ts rename to x-pack/plugins/security_solution/server/assistant/tools/esql/common.ts diff --git a/x-pack/plugins/security_solution/server/assistant/tools/esql_language_knowledge_base/nl_to_esql_tool.test.ts b/x-pack/plugins/security_solution/server/assistant/tools/esql/nl_to_esql_tool.test.ts similarity index 63% rename from x-pack/plugins/security_solution/server/assistant/tools/esql_language_knowledge_base/nl_to_esql_tool.test.ts rename to x-pack/plugins/security_solution/server/assistant/tools/esql/nl_to_esql_tool.test.ts index f078bccb24a36..10b1fa21daefe 100644 --- a/x-pack/plugins/security_solution/server/assistant/tools/esql_language_knowledge_base/nl_to_esql_tool.test.ts +++ b/x-pack/plugins/security_solution/server/assistant/tools/esql/nl_to_esql_tool.test.ts @@ -40,65 +40,18 @@ describe('NaturalLanguageESQLTool', () => { request, inference, connectorId, + isEnabledKnowledgeBase: true, }; describe('isSupported', () => { - it('returns false if isEnabledKnowledgeBase is false', () => { - const params = { - isEnabledKnowledgeBase: false, - modelExists: true, - ...rest, - }; - - expect(NL_TO_ESQL_TOOL.isSupported(params)).toBe(false); - }); - - it('returns false if modelExists is false (the ELSER model is not installed)', () => { - const params = { - isEnabledKnowledgeBase: true, - modelExists: false, // <-- ELSER model is not installed - ...rest, - }; - - expect(NL_TO_ESQL_TOOL.isSupported(params)).toBe(false); - }); - - it('returns true if isEnabledKnowledgeBase and modelExists are true', () => { - const params = { - isEnabledKnowledgeBase: true, - modelExists: true, - ...rest, - }; - - expect(NL_TO_ESQL_TOOL.isSupported(params)).toBe(true); + it('returns true if connectorId and inference have values', () => { + expect(NL_TO_ESQL_TOOL.isSupported(rest)).toBe(true); }); }); describe('getTool', () => { - it('returns null if isEnabledKnowledgeBase is false', () => { - const tool = NL_TO_ESQL_TOOL.getTool({ - isEnabledKnowledgeBase: false, - modelExists: true, - ...rest, - }); - - expect(tool).toBeNull(); - }); - - it('returns null if modelExists is false (the ELSER model is not installed)', () => { - const tool = NL_TO_ESQL_TOOL.getTool({ - isEnabledKnowledgeBase: true, - modelExists: false, // <-- ELSER model is not installed - ...rest, - }); - - expect(tool).toBeNull(); - }); - it('returns null if inference plugin is not provided', () => { const tool = NL_TO_ESQL_TOOL.getTool({ - isEnabledKnowledgeBase: true, - modelExists: true, ...rest, inference: undefined, }); @@ -108,8 +61,6 @@ describe('NaturalLanguageESQLTool', () => { it('returns null if connectorId is not provided', () => { const tool = NL_TO_ESQL_TOOL.getTool({ - isEnabledKnowledgeBase: true, - modelExists: true, ...rest, connectorId: undefined, }); @@ -117,10 +68,8 @@ describe('NaturalLanguageESQLTool', () => { expect(tool).toBeNull(); }); - it('should return a Tool instance if isEnabledKnowledgeBase and modelExists are true', () => { + it('should return a Tool instance when given required properties', () => { const tool = NL_TO_ESQL_TOOL.getTool({ - isEnabledKnowledgeBase: true, - modelExists: true, ...rest, }); @@ -129,8 +78,6 @@ describe('NaturalLanguageESQLTool', () => { it('should return a tool with the expected tags', () => { const tool = NL_TO_ESQL_TOOL.getTool({ - isEnabledKnowledgeBase: true, - modelExists: true, ...rest, }) as DynamicTool; @@ -139,8 +86,6 @@ describe('NaturalLanguageESQLTool', () => { it('should return tool with the expected description for OSS model', () => { const tool = NL_TO_ESQL_TOOL.getTool({ - isEnabledKnowledgeBase: true, - modelExists: true, isOssModel: true, ...rest, }) as DynamicTool; @@ -150,8 +95,6 @@ describe('NaturalLanguageESQLTool', () => { it('should return tool with the expected description for non-OSS model', () => { const tool = NL_TO_ESQL_TOOL.getTool({ - isEnabledKnowledgeBase: true, - modelExists: true, isOssModel: false, ...rest, }) as DynamicTool; diff --git a/x-pack/plugins/security_solution/server/assistant/tools/esql_language_knowledge_base/nl_to_esql_tool.ts b/x-pack/plugins/security_solution/server/assistant/tools/esql/nl_to_esql_tool.ts similarity index 94% rename from x-pack/plugins/security_solution/server/assistant/tools/esql_language_knowledge_base/nl_to_esql_tool.ts rename to x-pack/plugins/security_solution/server/assistant/tools/esql/nl_to_esql_tool.ts index 96b865efeaed4..1205fb03b0458 100644 --- a/x-pack/plugins/security_solution/server/assistant/tools/esql_language_knowledge_base/nl_to_esql_tool.ts +++ b/x-pack/plugins/security_solution/server/assistant/tools/esql/nl_to_esql_tool.ts @@ -13,6 +13,7 @@ import { naturalLanguageToEsql } from '@kbn/inference-plugin/server'; import { APP_UI_ID } from '../../../../common'; import { getPromptSuffixForOssModel } from './common'; +// select only some properties of AssistantToolParams export type ESQLToolParams = AssistantToolParams; const TOOL_NAME = 'NaturalLanguageESQLTool'; @@ -32,8 +33,8 @@ export const NL_TO_ESQL_TOOL: AssistantTool = { ...toolDetails, sourceRegister: APP_UI_ID, isSupported: (params: ESQLToolParams): params is ESQLToolParams => { - const { inference, connectorId, isEnabledKnowledgeBase, modelExists } = params; - return isEnabledKnowledgeBase && modelExists && inference != null && connectorId != null; + const { inference, connectorId } = params; + return inference != null && connectorId != null; }, getTool(params: ESQLToolParams) { if (!this.isSupported(params)) return null; diff --git a/x-pack/plugins/security_solution/server/assistant/tools/esql_language_knowledge_base/esql_language_knowledge_base_tool.test.ts b/x-pack/plugins/security_solution/server/assistant/tools/esql_language_knowledge_base/esql_language_knowledge_base_tool.test.ts deleted file mode 100644 index 589c95e8483bf..0000000000000 --- a/x-pack/plugins/security_solution/server/assistant/tools/esql_language_knowledge_base/esql_language_knowledge_base_tool.test.ts +++ /dev/null @@ -1,135 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license 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 { DynamicTool } from '@langchain/core/tools'; -import { ESQL_KNOWLEDGE_BASE_TOOL } from './esql_language_knowledge_base_tool'; -import type { ElasticsearchClient } from '@kbn/core-elasticsearch-server'; -import type { KibanaRequest } from '@kbn/core-http-server'; -import type { ExecuteConnectorRequestBody } from '@kbn/elastic-assistant-common/impl/schemas/actions_connector/post_actions_connector_execute_route.gen'; -import { loggerMock } from '@kbn/logging-mocks'; -import type { AIAssistantKnowledgeBaseDataClient } from '@kbn/elastic-assistant-plugin/server/ai_assistant_data_clients/knowledge_base'; -import { getPromptSuffixForOssModel } from './common'; - -describe('EsqlLanguageKnowledgeBaseTool', () => { - const kbDataClient = jest.fn() as unknown as AIAssistantKnowledgeBaseDataClient; - const esClient = { - search: jest.fn().mockResolvedValue({}), - } as unknown as ElasticsearchClient; - const request = { - body: { - isEnabledKnowledgeBase: false, - alertsIndexPattern: '.alerts-security.alerts-default', - allow: ['@timestamp', 'cloud.availability_zone', 'user.name'], - allowReplacement: ['user.name'], - replacements: { key: 'value' }, - size: 20, - }, - } as unknown as KibanaRequest; - const logger = loggerMock.create(); - const rest = { - kbDataClient, - esClient, - logger, - request, - }; - - describe('isSupported', () => { - it('returns false if isEnabledKnowledgeBase is false', () => { - const params = { - isEnabledKnowledgeBase: false, - modelExists: true, - ...rest, - }; - - expect(ESQL_KNOWLEDGE_BASE_TOOL.isSupported(params)).toBe(false); - }); - - it('returns false if modelExists is false (the ELSER model is not installed)', () => { - const params = { - isEnabledKnowledgeBase: true, - modelExists: false, // <-- ELSER model is not installed - ...rest, - }; - - expect(ESQL_KNOWLEDGE_BASE_TOOL.isSupported(params)).toBe(false); - }); - - it('returns true if isEnabledKnowledgeBase and modelExists are true', () => { - const params = { - isEnabledKnowledgeBase: true, - modelExists: true, - ...rest, - }; - - expect(ESQL_KNOWLEDGE_BASE_TOOL.isSupported(params)).toBe(true); - }); - }); - - describe('getTool', () => { - it('returns null if isEnabledKnowledgeBase is false', () => { - const tool = ESQL_KNOWLEDGE_BASE_TOOL.getTool({ - isEnabledKnowledgeBase: false, - modelExists: true, - ...rest, - }); - - expect(tool).toBeNull(); - }); - - it('returns null if modelExists is false (the ELSER model is not installed)', () => { - const tool = ESQL_KNOWLEDGE_BASE_TOOL.getTool({ - isEnabledKnowledgeBase: true, - modelExists: false, // <-- ELSER model is not installed - ...rest, - }); - - expect(tool).toBeNull(); - }); - - it('should return a Tool instance if isEnabledKnowledgeBase and modelExists are true', () => { - const tool = ESQL_KNOWLEDGE_BASE_TOOL.getTool({ - isEnabledKnowledgeBase: true, - modelExists: true, - ...rest, - }); - - expect(tool?.name).toEqual('ESQLKnowledgeBaseTool'); - }); - - it('should return a tool with the expected tags', () => { - const tool = ESQL_KNOWLEDGE_BASE_TOOL.getTool({ - isEnabledKnowledgeBase: true, - modelExists: true, - ...rest, - }) as DynamicTool; - - expect(tool.tags).toEqual(['esql', 'query-generation', 'knowledge-base']); - }); - - it('should return tool with the expected description for OSS model', () => { - const tool = ESQL_KNOWLEDGE_BASE_TOOL.getTool({ - isEnabledKnowledgeBase: true, - modelExists: true, - isOssModel: true, - ...rest, - }) as DynamicTool; - - expect(tool.description).toContain(getPromptSuffixForOssModel('ESQLKnowledgeBaseTool')); - }); - - it('should return tool with the expected description for non-OSS model', () => { - const tool = ESQL_KNOWLEDGE_BASE_TOOL.getTool({ - isEnabledKnowledgeBase: true, - modelExists: true, - isOssModel: false, - ...rest, - }) as DynamicTool; - - expect(tool.description).not.toContain(getPromptSuffixForOssModel('ESQLKnowledgeBaseTool')); - }); - }); -}); diff --git a/x-pack/plugins/security_solution/server/assistant/tools/esql_language_knowledge_base/esql_language_knowledge_base_tool.ts b/x-pack/plugins/security_solution/server/assistant/tools/esql_language_knowledge_base/esql_language_knowledge_base_tool.ts deleted file mode 100644 index 37e037898cd20..0000000000000 --- a/x-pack/plugins/security_solution/server/assistant/tools/esql_language_knowledge_base/esql_language_knowledge_base_tool.ts +++ /dev/null @@ -1,80 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { DynamicStructuredTool } from '@langchain/core/tools'; -import { DirectoryLoader } from 'langchain/document_loaders/fs/directory'; -import { TextLoader } from 'langchain/document_loaders/fs/text'; -import type { Document } from 'langchain/document'; -import { resolve } from 'path'; -import { z } from '@kbn/zod'; -import type { AssistantTool, AssistantToolParams } from '@kbn/elastic-assistant-plugin/server'; -import { ESQL_RESOURCE } from '@kbn/elastic-assistant-plugin/server/routes/knowledge_base/constants'; -import { APP_UI_ID } from '../../../../common'; -import { getPromptSuffixForOssModel } from './common'; - -const TOOL_NAME = 'ESQLKnowledgeBaseTool'; - -const toolDetails = { - id: 'esql-knowledge-base-tool', - name: TOOL_NAME, - description: - 'Call this for knowledge on how to build an ESQL query, or answer questions about the ES|QL query language. Input must always be the user query on a single line, with no other text. Your answer will be parsed as JSON, so never use quotes within the output and instead use backticks. Do not add any additional text to describe your output.', -}; -export const ESQL_KNOWLEDGE_BASE_TOOL: AssistantTool = { - ...toolDetails, - sourceRegister: APP_UI_ID, - isSupported: (params: AssistantToolParams): params is AssistantToolParams => { - const { kbDataClient, isEnabledKnowledgeBase, modelExists } = params; - return isEnabledKnowledgeBase && modelExists && kbDataClient != null; - }, - getTool(params: AssistantToolParams) { - if (!this.isSupported(params)) return null; - - const { kbDataClient, isOssModel } = params as AssistantToolParams; - if (kbDataClient == null) return null; - - return new DynamicStructuredTool({ - name: toolDetails.name, - description: - toolDetails.description + (isOssModel ? getPromptSuffixForOssModel(TOOL_NAME) : ''), - schema: z.object({ - question: z.string().describe(`The user's exact question about ESQL`), - }), - func: async (input) => { - const exampleQueriesLoader = new DirectoryLoader( - resolve( - __dirname, - '../../../../../elastic_assistant/server/knowledge_base/esql/example_queries' - ), - { - '.asciidoc': (path) => new TextLoader(path), - }, - true - ); - const rawExampleQueries = await exampleQueriesLoader.load(); - - const docs = await kbDataClient.getKnowledgeBaseDocumentEntries({ - kbResource: ESQL_RESOURCE, - query: input.question, - }); - - let legacyDocs: Document[] = []; - - if (!kbDataClient?.isV2KnowledgeBaseEnabled) { - legacyDocs = await kbDataClient.getKnowledgeBaseDocumentEntries({ - kbResource: 'unknown', - query: input.question, - }); - } - - return JSON.stringify([...rawExampleQueries, ...docs, ...legacyDocs]).substring(0, 50000); - }, - tags: ['esql', 'query-generation', 'knowledge-base'], - // TODO: Remove after ZodAny is fixed https://github.com/langchain-ai/langchainjs/blob/main/langchain-core/src/tools.ts - }) as unknown as DynamicStructuredTool; - }, -}; diff --git a/x-pack/plugins/security_solution/server/assistant/tools/index.test.ts b/x-pack/plugins/security_solution/server/assistant/tools/index.test.ts index 0d719adc80fe2..70d0daea037ed 100644 --- a/x-pack/plugins/security_solution/server/assistant/tools/index.test.ts +++ b/x-pack/plugins/security_solution/server/assistant/tools/index.test.ts @@ -13,7 +13,7 @@ describe('getAssistantTools', () => { }); it('should return an array of applicable tools', () => { - const tools = getAssistantTools({ naturalLanguageESQLToolEnabled: true }); + const tools = getAssistantTools({}); const minExpectedTools = 3; // 3 tools are currently implemented diff --git a/x-pack/plugins/security_solution/server/assistant/tools/index.ts b/x-pack/plugins/security_solution/server/assistant/tools/index.ts index fa0098dce1eec..a704aaa44d0a1 100644 --- a/x-pack/plugins/security_solution/server/assistant/tools/index.ts +++ b/x-pack/plugins/security_solution/server/assistant/tools/index.ts @@ -7,8 +7,7 @@ import type { AssistantTool } from '@kbn/elastic-assistant-plugin/server'; -import { ESQL_KNOWLEDGE_BASE_TOOL } from './esql_language_knowledge_base/esql_language_knowledge_base_tool'; -import { NL_TO_ESQL_TOOL } from './esql_language_knowledge_base/nl_to_esql_tool'; +import { NL_TO_ESQL_TOOL } from './esql/nl_to_esql_tool'; import { ALERT_COUNTS_TOOL } from './alert_counts/alert_counts_tool'; import { OPEN_AND_ACKNOWLEDGED_ALERTS_TOOL } from './open_and_acknowledged_alerts/open_and_acknowledged_alerts_tool'; import { ATTACK_DISCOVERY_TOOL } from './attack_discovery/attack_discovery_tool'; @@ -17,16 +16,14 @@ import { KNOWLEDGE_BASE_WRITE_TOOL } from './knowledge_base/knowledge_base_write import { SECURITY_LABS_KNOWLEDGE_BASE_TOOL } from './security_labs/security_labs_tool'; export const getAssistantTools = ({ - naturalLanguageESQLToolEnabled, assistantKnowledgeBaseByDefault, }: { - naturalLanguageESQLToolEnabled?: boolean; assistantKnowledgeBaseByDefault?: boolean; }): AssistantTool[] => { const tools = [ ALERT_COUNTS_TOOL, ATTACK_DISCOVERY_TOOL, - naturalLanguageESQLToolEnabled ? NL_TO_ESQL_TOOL : ESQL_KNOWLEDGE_BASE_TOOL, + NL_TO_ESQL_TOOL, KNOWLEDGE_BASE_RETRIEVAL_TOOL, KNOWLEDGE_BASE_WRITE_TOOL, OPEN_AND_ACKNOWLEDGED_ALERTS_TOOL, diff --git a/x-pack/plugins/security_solution/server/assistant/tools/knowledge_base/knowledge_base_retrieval_tool.ts b/x-pack/plugins/security_solution/server/assistant/tools/knowledge_base/knowledge_base_retrieval_tool.ts index 7739de18857aa..cea2bdadf5970 100644 --- a/x-pack/plugins/security_solution/server/assistant/tools/knowledge_base/knowledge_base_retrieval_tool.ts +++ b/x-pack/plugins/security_solution/server/assistant/tools/knowledge_base/knowledge_base_retrieval_tool.ts @@ -25,8 +25,8 @@ export const KNOWLEDGE_BASE_RETRIEVAL_TOOL: AssistantTool = { ...toolDetails, sourceRegister: APP_UI_ID, isSupported: (params: AssistantToolParams): params is KnowledgeBaseRetrievalToolParams => { - const { kbDataClient, isEnabledKnowledgeBase, modelExists } = params; - return isEnabledKnowledgeBase && modelExists && kbDataClient != null; + const { kbDataClient, isEnabledKnowledgeBase } = params; + return isEnabledKnowledgeBase && kbDataClient != null; }, getTool(params: AssistantToolParams) { if (!this.isSupported(params)) return null; diff --git a/x-pack/plugins/security_solution/server/assistant/tools/knowledge_base/knowledge_base_write_tool.ts b/x-pack/plugins/security_solution/server/assistant/tools/knowledge_base/knowledge_base_write_tool.ts index 9b46c625e115b..4069eeeef5b97 100644 --- a/x-pack/plugins/security_solution/server/assistant/tools/knowledge_base/knowledge_base_write_tool.ts +++ b/x-pack/plugins/security_solution/server/assistant/tools/knowledge_base/knowledge_base_write_tool.ts @@ -28,8 +28,8 @@ export const KNOWLEDGE_BASE_WRITE_TOOL: AssistantTool = { ...toolDetails, sourceRegister: APP_UI_ID, isSupported: (params: AssistantToolParams): params is KnowledgeBaseWriteToolParams => { - const { isEnabledKnowledgeBase, kbDataClient, modelExists } = params; - return isEnabledKnowledgeBase && modelExists && kbDataClient != null; + const { isEnabledKnowledgeBase, kbDataClient } = params; + return isEnabledKnowledgeBase && kbDataClient != null; }, getTool(params: AssistantToolParams) { if (!this.isSupported(params)) return null; diff --git a/x-pack/plugins/security_solution/server/assistant/tools/open_and_acknowledged_alerts/open_and_acknowledged_alerts_tool.test.ts b/x-pack/plugins/security_solution/server/assistant/tools/open_and_acknowledged_alerts/open_and_acknowledged_alerts_tool.test.ts index 2b134dfd86335..09bae1639f1b1 100644 --- a/x-pack/plugins/security_solution/server/assistant/tools/open_and_acknowledged_alerts/open_and_acknowledged_alerts_tool.test.ts +++ b/x-pack/plugins/security_solution/server/assistant/tools/open_and_acknowledged_alerts/open_and_acknowledged_alerts_tool.test.ts @@ -32,14 +32,12 @@ describe('OpenAndAcknowledgedAlertsTool', () => { } as unknown as KibanaRequest; const isEnabledKnowledgeBase = true; const chain = {} as unknown as RetrievalQAChain; - const modelExists = true; const logger = loggerMock.create(); const rest = { isEnabledKnowledgeBase, esClient, chain, logger, - modelExists, }; const anonymizationFields = [ diff --git a/x-pack/plugins/security_solution/server/assistant/tools/security_labs/security_labs_tool.ts b/x-pack/plugins/security_solution/server/assistant/tools/security_labs/security_labs_tool.ts index 70e955dda8470..48e1619c2f00f 100644 --- a/x-pack/plugins/security_solution/server/assistant/tools/security_labs/security_labs_tool.ts +++ b/x-pack/plugins/security_solution/server/assistant/tools/security_labs/security_labs_tool.ts @@ -22,8 +22,8 @@ export const SECURITY_LABS_KNOWLEDGE_BASE_TOOL: AssistantTool = { ...toolDetails, sourceRegister: APP_UI_ID, isSupported: (params: AssistantToolParams): params is AssistantToolParams => { - const { kbDataClient, isEnabledKnowledgeBase, modelExists } = params; - return isEnabledKnowledgeBase && modelExists && kbDataClient != null; + const { kbDataClient, isEnabledKnowledgeBase } = params; + return isEnabledKnowledgeBase && kbDataClient != null; }, getTool(params: AssistantToolParams) { if (!this.isSupported(params)) return null; diff --git a/x-pack/plugins/security_solution/server/endpoint/routes/metadata/index.ts b/x-pack/plugins/security_solution/server/endpoint/routes/metadata/index.ts index 75d0fb2135fc8..9603e9e9a6d48 100644 --- a/x-pack/plugins/security_solution/server/endpoint/routes/metadata/index.ts +++ b/x-pack/plugins/security_solution/server/endpoint/routes/metadata/index.ts @@ -23,6 +23,7 @@ import type { SecuritySolutionPluginRouter } from '../../../types'; import { HOST_METADATA_GET_ROUTE, HOST_METADATA_LIST_ROUTE, + METADATA_TRANSFORMS_STATUS_INTERNAL_ROUTE, METADATA_TRANSFORMS_STATUS_ROUTE, } from '../../../../common/endpoint/constants'; import { withEndpointAuthz } from '../with_endpoint_authz'; @@ -94,6 +95,7 @@ export function registerEndpointRoutes( access: 'public', path: METADATA_TRANSFORMS_STATUS_ROUTE, options: { authRequired: true, tags: ['access:securitySolution'] }, + deprecated: true, }) .addVersion( { @@ -106,4 +108,22 @@ export function registerEndpointRoutes( getMetadataTransformStatsHandler(endpointAppContext, logger) ) ); + + router.versioned + .get({ + access: 'internal', + path: METADATA_TRANSFORMS_STATUS_INTERNAL_ROUTE, + options: { authRequired: true, tags: ['access:securitySolution'] }, + }) + .addVersion( + { + version: '1', + validate: false, + }, + withEndpointAuthz( + { all: ['canReadSecuritySolution'] }, + logger, + getMetadataTransformStatsHandler(endpointAppContext, logger) + ) + ); } diff --git a/x-pack/plugins/security_solution/server/endpoint/routes/metadata/metadata.test.ts b/x-pack/plugins/security_solution/server/endpoint/routes/metadata/metadata.test.ts index 63d3c466dd2b6..dbf60ef127c22 100644 --- a/x-pack/plugins/security_solution/server/endpoint/routes/metadata/metadata.test.ts +++ b/x-pack/plugins/security_solution/server/endpoint/routes/metadata/metadata.test.ts @@ -47,7 +47,7 @@ import { ENDPOINT_DEFAULT_SORT_FIELD, HOST_METADATA_GET_ROUTE, HOST_METADATA_LIST_ROUTE, - METADATA_TRANSFORMS_STATUS_ROUTE, + METADATA_TRANSFORMS_STATUS_INTERNAL_ROUTE, METADATA_UNITED_INDEX, } from '../../../../common/endpoint/constants'; import { TRANSFORM_STATES } from '../../../../common/constants'; @@ -506,8 +506,8 @@ describe('test endpoint routes', () => { ({ routeConfig, routeHandler } = getRegisteredVersionedRouteMock( routerMock, 'get', - METADATA_TRANSFORMS_STATUS_ROUTE, - '2023-10-31' + METADATA_TRANSFORMS_STATUS_INTERNAL_ROUTE, + '1' )); const contextOverrides = { @@ -539,8 +539,8 @@ describe('test endpoint routes', () => { ({ routeConfig, routeHandler } = getRegisteredVersionedRouteMock( routerMock, 'get', - METADATA_TRANSFORMS_STATUS_ROUTE, - '2023-10-31' + METADATA_TRANSFORMS_STATUS_INTERNAL_ROUTE, + '1' )); await routeHandler( createRouteHandlerContext(mockScopedClient, mockSavedObjectClient), diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/prebuilt_rules/model/rule_assets/prebuilt_rule_asset.mock.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/prebuilt_rules/model/rule_assets/prebuilt_rule_asset.mock.ts index c73203c2871ab..8f9c1a6a32357 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/prebuilt_rules/model/rule_assets/prebuilt_rule_asset.mock.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/prebuilt_rules/model/rule_assets/prebuilt_rule_asset.mock.ts @@ -18,6 +18,7 @@ export const getPrebuiltRuleMock = (rewrites?: Partial): Preb language: 'kuery', rule_id: 'rule-1', version: 1, + author: [], ...rewrites, } as PrebuiltRuleAsset); @@ -51,6 +52,7 @@ export const getPrebuiltThreatMatchRuleMock = (): PrebuiltRuleAsset => ({ language: 'kuery', rule_id: 'rule-1', version: 1, + author: [], threat_query: '*:*', threat_index: ['list-index'], threat_mapping: [ diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/prebuilt_rules/model/rule_assets/prebuilt_rule_asset.test.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/prebuilt_rules/model/rule_assets/prebuilt_rule_asset.test.ts index 45a561996e0a9..5d963db71cdea 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/prebuilt_rules/model/rule_assets/prebuilt_rule_asset.test.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/prebuilt_rules/model/rule_assets/prebuilt_rule_asset.test.ts @@ -7,23 +7,10 @@ import { expectParseError, expectParseSuccess, stringifyZodError } from '@kbn/zod-helpers'; import { getListArrayMock } from '../../../../../../common/detection_engine/schemas/types/lists.mock'; -import { PrebuiltRuleAsset, TypeSpecificFields } from './prebuilt_rule_asset'; +import { PrebuiltRuleAsset } from './prebuilt_rule_asset'; import { getPrebuiltRuleMock, getPrebuiltThreatMatchRuleMock } from './prebuilt_rule_asset.mock'; -import { TypeSpecificCreatePropsInternal } from '../../../../../../common/api/detection_engine'; describe('Prebuilt rule asset schema', () => { - it('can be of all rule types that are supported', () => { - // Check that the discriminated union TypeSpecificFields, which is used to create - // the PrebuiltRuleAsset schema, contains all the rule types that are supported. - const createPropsTypes = TypeSpecificCreatePropsInternal.options.map( - (option) => option.shape.type.value - ); - const fieldsTypes = TypeSpecificFields.options.map((option) => option.shape.type.value); - - expect(createPropsTypes).toHaveLength(fieldsTypes.length); - expect(new Set(createPropsTypes)).toEqual(new Set(fieldsTypes)); - }); - test('empty objects do not validate', () => { const payload: Partial = {}; diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/prebuilt_rules/model/rule_assets/prebuilt_rule_asset.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/prebuilt_rules/model/rule_assets/prebuilt_rule_asset.ts index 2d7b056f86248..cc7e38632547f 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/prebuilt_rules/model/rule_assets/prebuilt_rule_asset.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/prebuilt_rules/model/rule_assets/prebuilt_rule_asset.ts @@ -6,20 +6,11 @@ */ import * as z from '@kbn/zod'; -import type { IsEqual } from 'type-fest'; -import type { TypeSpecificCreateProps } from '../../../../../../common/api/detection_engine/model/rule_schema'; import { RuleSignatureId, RuleVersion, BaseCreateProps, - EqlRuleCreateFields, - EsqlRuleCreateFields, - MachineLearningRuleCreateFields, - NewTermsRuleCreateFields, - QueryRuleCreateFields, - SavedQueryRuleCreateFields, - ThreatMatchRuleCreateFields, - ThresholdRuleCreateFields, + TypeSpecificCreatePropsInternal, } from '../../../../../../common/api/detection_engine/model/rule_schema'; function zodMaskFor() { @@ -38,6 +29,7 @@ function zodMaskFor() { */ const BASE_PROPS_REMOVED_FROM_PREBUILT_RULE_ASSET = zodMaskFor()([ 'actions', + 'response_actions', 'throttle', 'meta', 'output_index', @@ -47,40 +39,6 @@ const BASE_PROPS_REMOVED_FROM_PREBUILT_RULE_ASSET = zodMaskFor( 'outcome', ]); -/** - * Aditionally remove fields which are part only of the optional fields in the rule types that make up - * the TypeSpecificCreateProps discriminatedUnion, by recreating a discriminated union of the types, but - * with the necessary fields omitted, in the types where they exist. Fields to extract: - * - response_actions: from Query and SavedQuery rules - */ -const TYPE_SPECIFIC_FIELDS_TO_OMIT = ['response_actions'] as const; - -const TYPE_SPECIFIC_FIELDS_TO_OMIT_FROM_QUERY_RULES = zodMaskFor()([ - ...TYPE_SPECIFIC_FIELDS_TO_OMIT, -]); -const TYPE_SPECIFIC_FIELDS_TO_OMIT_FROM_SAVED_QUERY_RULES = - zodMaskFor()([...TYPE_SPECIFIC_FIELDS_TO_OMIT]); - -export type TypeSpecificFields = z.infer; -export const TypeSpecificFields = z.discriminatedUnion('type', [ - EqlRuleCreateFields.omit(TYPE_SPECIFIC_FIELDS_TO_OMIT_FROM_QUERY_RULES), - QueryRuleCreateFields.omit(TYPE_SPECIFIC_FIELDS_TO_OMIT_FROM_QUERY_RULES), - SavedQueryRuleCreateFields.omit(TYPE_SPECIFIC_FIELDS_TO_OMIT_FROM_SAVED_QUERY_RULES), - ThresholdRuleCreateFields, - ThreatMatchRuleCreateFields, - MachineLearningRuleCreateFields, - NewTermsRuleCreateFields.omit(TYPE_SPECIFIC_FIELDS_TO_OMIT_FROM_QUERY_RULES), - EsqlRuleCreateFields.omit(TYPE_SPECIFIC_FIELDS_TO_OMIT_FROM_QUERY_RULES), -]); - -// Make sure the type-specific fields contain all the same rule types as the type-specific rule params. -// TS will throw a type error if the types are not equal (for example, if a new rule type is added to -// the TypeSpecificCreateProps and the new type is not reflected in TypeSpecificFields). -export const areTypesEqual: IsEqual< - typeof TypeSpecificCreateProps._type.type, - typeof TypeSpecificFields._type.type -> = true; - export const PrebuiltAssetBaseProps = BaseCreateProps.omit( BASE_PROPS_REMOVED_FROM_PREBUILT_RULE_ASSET ); @@ -101,7 +59,7 @@ export const PrebuiltAssetBaseProps = BaseCreateProps.omit( * - some fields are omitted because they are not present in https://github.com/elastic/detection-rules */ export type PrebuiltRuleAsset = z.infer; -export const PrebuiltRuleAsset = PrebuiltAssetBaseProps.and(TypeSpecificFields).and( +export const PrebuiltRuleAsset = PrebuiltAssetBaseProps.and(TypeSpecificCreatePropsInternal).and( z.object({ rule_id: RuleSignatureId, version: RuleVersion, @@ -112,7 +70,7 @@ function createUpgradableRuleFieldsPayloadByType() { const baseFields = Object.keys(PrebuiltAssetBaseProps.shape); return new Map( - TypeSpecificFields.options.map((option) => { + TypeSpecificCreatePropsInternal.options.map((option) => { const typeName = option.shape.type.value; const typeSpecificFieldsForType = Object.keys(option.shape); diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_management/logic/bulk_actions/validations.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_management/logic/bulk_actions/validations.ts index d89c78be6b846..8e732ffe6509f 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_management/logic/bulk_actions/validations.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_management/logic/bulk_actions/validations.ts @@ -90,11 +90,6 @@ export const validateBulkScheduleBackfill = async ({ experimentalFeatures, }: DryRunManualRuleRunBulkActionsValidationArgs) => { // check whether "manual rule run" feature is enabled - await throwDryRunError( - () => - invariant(experimentalFeatures?.manualRuleRunEnabled, 'Manual rule run feature is disabled.'), - BulkActionsDryRunErrCode.MANUAL_RULE_RUN_FEATURE - ); await throwDryRunError( () => invariant(rule.enabled, 'Cannot schedule manual rule run for a disabled rule'), diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_management/logic/detection_rules_client/converters/common_params_camel_to_snake.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_management/logic/detection_rules_client/converters/common_params_camel_to_snake.ts index f86abd4f08d8d..38e40ab67611f 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_management/logic/detection_rules_client/converters/common_params_camel_to_snake.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_management/logic/detection_rules_client/converters/common_params_camel_to_snake.ts @@ -6,6 +6,7 @@ */ import snakecaseKeys from 'snakecase-keys'; +import { transformAlertToRuleResponseAction } from '../../../../../../../common/detection_engine/transform_actions'; import { convertObjectKeysToSnakeCase } from '../../../../../../utils/object_case_converters'; import type { BaseRuleParams } from '../../../../rule_schema'; import { migrateLegacyInvestigationFields } from '../../../utils/utils'; @@ -44,6 +45,7 @@ export const commonParamsCamelToSnake = (params: BaseRuleParams) => { rule_source: convertObjectKeysToSnakeCase(params.ruleSource), related_integrations: params.relatedIntegrations ?? [], required_fields: params.requiredFields ?? [], + response_actions: params.responseActions?.map(transformAlertToRuleResponseAction), setup: params.setup ?? '', }; }; diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_management/logic/detection_rules_client/converters/convert_rule_response_to_alerting_rule.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_management/logic/detection_rules_client/converters/convert_rule_response_to_alerting_rule.ts index 2348c11027c65..0c2edf5535f35 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_management/logic/detection_rules_client/converters/convert_rule_response_to_alerting_rule.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_management/logic/detection_rules_client/converters/convert_rule_response_to_alerting_rule.ts @@ -53,6 +53,9 @@ export const convertRuleResponseToAlertingRule = ( const alertActions = ruleActions?.map((action) => transformRuleToAlertAction(action)) ?? []; const actions = transformToActionFrequency(alertActions as RuleActionCamel[], rule.throttle); + const responseActions = rule.response_actions?.map((ruleResponseAction) => + transformRuleToAlertResponseAction(ruleResponseAction) + ); // Because of Omit Typescript doesn't recognize // that rule is assignable to TypeSpecificCreateProps despite omitted fields // are not part of type specific props. So we need to cast here. @@ -94,6 +97,7 @@ export const convertRuleResponseToAlertingRule = ( note: rule.note, version: rule.version, exceptionsList: rule.exceptions_list, + responseActions, ...typeSpecificParams, }, schedule: { interval: rule.interval }, @@ -119,9 +123,6 @@ const typeSpecificSnakeToCamel = (params: TypeSpecificCreateProps): TypeSpecific eventCategoryOverride: params.event_category_override, tiebreakerField: params.tiebreaker_field, alertSuppression: convertObjectKeysToCamelCase(params.alert_suppression), - responseActions: params.response_actions?.map((rule) => - transformRuleToAlertResponseAction(rule) - ), }; } case 'esql': { @@ -130,9 +131,6 @@ const typeSpecificSnakeToCamel = (params: TypeSpecificCreateProps): TypeSpecific language: params.language, query: params.query, alertSuppression: convertObjectKeysToCamelCase(params.alert_suppression), - responseActions: params.response_actions?.map((rule) => - transformRuleToAlertResponseAction(rule) - ), }; } case 'threat_match': { @@ -164,9 +162,6 @@ const typeSpecificSnakeToCamel = (params: TypeSpecificCreateProps): TypeSpecific query: params.query ?? '', filters: params.filters, savedId: params.saved_id, - responseActions: params.response_actions?.map((rule) => - transformRuleToAlertResponseAction(rule) - ), alertSuppression: convertObjectKeysToCamelCase(params.alert_suppression), }; } @@ -216,9 +211,6 @@ const typeSpecificSnakeToCamel = (params: TypeSpecificCreateProps): TypeSpecific language: params.language ?? 'kuery', dataViewId: params.data_view_id, alertSuppression: convertObjectKeysToCamelCase(params.alert_suppression), - responseActions: params.response_actions?.map((rule) => - transformRuleToAlertResponseAction(rule) - ), }; } default: { diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_management/logic/detection_rules_client/converters/type_specific_camel_to_snake.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_management/logic/detection_rules_client/converters/type_specific_camel_to_snake.ts index a4b74e31ba291..5a2f7ba0d3548 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_management/logic/detection_rules_client/converters/type_specific_camel_to_snake.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_management/logic/detection_rules_client/converters/type_specific_camel_to_snake.ts @@ -6,7 +6,6 @@ */ import type { RequiredOptional } from '@kbn/zod-helpers'; -import { transformAlertToRuleResponseAction } from '../../../../../../../common/detection_engine/transform_actions'; import type { TypeSpecificResponse } from '../../../../../../../common/api/detection_engine/model/rule_schema'; import { assertUnreachable } from '../../../../../../../common/utility_types'; import { convertObjectKeysToSnakeCase } from '../../../../../../utils/object_case_converters'; @@ -28,7 +27,6 @@ export const typeSpecificCamelToSnake = ( event_category_override: params.eventCategoryOverride, tiebreaker_field: params.tiebreakerField, alert_suppression: convertObjectKeysToSnakeCase(params.alertSuppression), - response_actions: params.responseActions?.map(transformAlertToRuleResponseAction), }; } case 'esql': { @@ -37,7 +35,6 @@ export const typeSpecificCamelToSnake = ( language: params.language, query: params.query, alert_suppression: convertObjectKeysToSnakeCase(params.alertSuppression), - response_actions: params.responseActions?.map(transformAlertToRuleResponseAction), }; } case 'threat_match': { @@ -69,7 +66,6 @@ export const typeSpecificCamelToSnake = ( query: params.query, filters: params.filters, saved_id: params.savedId, - response_actions: params.responseActions?.map(transformAlertToRuleResponseAction), alert_suppression: convertObjectKeysToSnakeCase(params.alertSuppression), }; } @@ -82,7 +78,6 @@ export const typeSpecificCamelToSnake = ( filters: params.filters, saved_id: params.savedId, data_view_id: params.dataViewId, - response_actions: params.responseActions?.map(transformAlertToRuleResponseAction), alert_suppression: convertObjectKeysToSnakeCase(params.alertSuppression), }; } @@ -120,7 +115,6 @@ export const typeSpecificCamelToSnake = ( language: params.language, data_view_id: params.dataViewId, alert_suppression: convertObjectKeysToSnakeCase(params.alertSuppression), - response_actions: params.responseActions?.map(transformAlertToRuleResponseAction), }; } default: { diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_management/logic/detection_rules_client/detection_rules_client.patch_rule.test.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_management/logic/detection_rules_client/detection_rules_client.patch_rule.test.ts index e460581c02a1c..448df6b581a3b 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_management/logic/detection_rules_client/detection_rules_client.patch_rule.test.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_management/logic/detection_rules_client/detection_rules_client.patch_rule.test.ts @@ -277,6 +277,27 @@ describe('DetectionRulesClient.patchRule', () => { expect(rulesClient.create).not.toHaveBeenCalled(); }); + it('throws an error if rule has external rule source and non-customizable fields are changed', async () => { + // Mock the existing rule + const existingRule = { + ...getRulesSchemaMock(), + rule_source: { type: 'external', is_customized: true }, + }; + (getRuleByRuleId as jest.Mock).mockResolvedValueOnce(existingRule); + + // Mock the rule update + const rulePatch = getCreateRulesSchemaMock('query-rule-id'); + rulePatch.license = 'new license'; + + // Mock the rule returned after update; not used for this test directly but + // needed so that the patchRule method does not throw + rulesClient.update.mockResolvedValue(getRuleMock(getQueryRuleParams())); + + await expect(detectionRulesClient.patchRule({ rulePatch })).rejects.toThrow( + 'Cannot update "license" field for prebuilt rules' + ); + }); + describe('actions', () => { it("updates the rule's actions if provided", async () => { // Mock the existing rule diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_management/logic/detection_rules_client/detection_rules_client.update_rule.test.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_management/logic/detection_rules_client/detection_rules_client.update_rule.test.ts index a660e5c5e8747..cbd0fb1fe3680 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_management/logic/detection_rules_client/detection_rules_client.update_rule.test.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_management/logic/detection_rules_client/detection_rules_client.update_rule.test.ts @@ -498,5 +498,26 @@ describe('DetectionRulesClient.updateRule', () => { }) ); }); + + it('throws an error if rule has external rule source and non-customizable fields are changed', async () => { + // Mock the existing rule + const existingRule = { + ...getRulesSchemaMock(), + rule_source: { type: 'external', is_customized: true }, + }; + + (getRuleByRuleId as jest.Mock).mockResolvedValueOnce(existingRule); + + // Mock the rule update + const ruleUpdate = { ...getCreateRulesSchemaMock(), author: ['new user'] }; + + // Mock the rule returned after update; not used for this test directly but + // needed so that the patchRule method does not throw + rulesClient.update.mockResolvedValue(getRuleMock(getQueryRuleParams())); + + await expect(detectionRulesClient.updateRule({ ruleUpdate })).rejects.toThrow( + 'Cannot update "author" field for prebuilt rules' + ); + }); }); }); diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_management/logic/detection_rules_client/mergers/apply_rule_defaults.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_management/logic/detection_rules_client/mergers/apply_rule_defaults.ts index 388b1ab695269..40f0b3eca3b98 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_management/logic/detection_rules_client/mergers/apply_rule_defaults.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_management/logic/detection_rules_client/mergers/apply_rule_defaults.ts @@ -86,7 +86,6 @@ export const setTypeSpecificDefaults = (props: TypeSpecificCreateProps) => { event_category_override: props.event_category_override, tiebreaker_field: props.tiebreaker_field, alert_suppression: props.alert_suppression, - response_actions: props.response_actions, }; } case 'esql': { @@ -95,7 +94,6 @@ export const setTypeSpecificDefaults = (props: TypeSpecificCreateProps) => { language: props.language, query: props.query, alert_suppression: props.alert_suppression, - response_actions: props.response_actions, }; } case 'threat_match': { @@ -127,7 +125,6 @@ export const setTypeSpecificDefaults = (props: TypeSpecificCreateProps) => { query: props.query ?? '', filters: props.filters, saved_id: props.saved_id, - response_actions: props.response_actions, alert_suppression: props.alert_suppression, }; } @@ -140,7 +137,6 @@ export const setTypeSpecificDefaults = (props: TypeSpecificCreateProps) => { filters: props.filters, saved_id: props.saved_id, data_view_id: props.data_view_id, - response_actions: props.response_actions, alert_suppression: props.alert_suppression, }; } @@ -178,7 +174,6 @@ export const setTypeSpecificDefaults = (props: TypeSpecificCreateProps) => { language: props.language ?? 'kuery', data_view_id: props.data_view_id, alert_suppression: props.alert_suppression, - response_actions: props.response_actions, }; } default: { diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_management/logic/detection_rules_client/mergers/apply_rule_patch.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_management/logic/detection_rules_client/mergers/apply_rule_patch.ts index d864170746ed3..ba21037ba376f 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_management/logic/detection_rules_client/mergers/apply_rule_patch.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_management/logic/detection_rules_client/mergers/apply_rule_patch.ts @@ -111,6 +111,7 @@ export const applyRulePatch = async ({ interval: rulePatch.interval ?? existingRule.interval, throttle: rulePatch.throttle ?? existingRule.throttle, actions: rulePatch.actions ?? existingRule.actions, + response_actions: rulePatch.response_actions ?? existingRule.response_actions, ...typeSpecificParams, }; @@ -138,7 +139,6 @@ const patchEqlParams = ( rulePatch.event_category_override ?? existingRule.event_category_override, tiebreaker_field: rulePatch.tiebreaker_field ?? existingRule.tiebreaker_field, alert_suppression: rulePatch.alert_suppression ?? existingRule.alert_suppression, - response_actions: rulePatch.response_actions ?? existingRule.response_actions, }; }; @@ -151,7 +151,6 @@ const patchEsqlParams = ( language: rulePatch.language ?? existingRule.language, query: rulePatch.query ?? existingRule.query, alert_suppression: rulePatch.alert_suppression ?? existingRule.alert_suppression, - response_actions: rulePatch.response_actions ?? existingRule.response_actions, }; }; @@ -191,7 +190,6 @@ const patchQueryParams = ( query: rulePatch.query ?? existingRule.query, filters: rulePatch.filters ?? existingRule.filters, saved_id: rulePatch.saved_id ?? existingRule.saved_id, - response_actions: rulePatch.response_actions ?? existingRule.response_actions, alert_suppression: rulePatch.alert_suppression ?? existingRule.alert_suppression, }; }; @@ -208,7 +206,6 @@ const patchSavedQueryParams = ( query: rulePatch.query ?? existingRule.query, filters: rulePatch.filters ?? existingRule.filters, saved_id: rulePatch.saved_id ?? existingRule.saved_id, - response_actions: rulePatch.response_actions ?? existingRule.response_actions, alert_suppression: rulePatch.alert_suppression ?? existingRule.alert_suppression, }; }; @@ -260,7 +257,6 @@ const patchNewTermsParams = ( new_terms_fields: params.new_terms_fields ?? existingRule.new_terms_fields, history_window_start: params.history_window_start ?? existingRule.history_window_start, alert_suppression: params.alert_suppression ?? existingRule.alert_suppression, - response_actions: params.response_actions ?? existingRule.response_actions, }; }; diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_management/logic/detection_rules_client/methods/patch_rule.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_management/logic/detection_rules_client/methods/patch_rule.ts index 1218991bf388e..113576e8d02e2 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_management/logic/detection_rules_client/methods/patch_rule.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_management/logic/detection_rules_client/methods/patch_rule.ts @@ -16,6 +16,7 @@ import type { MlAuthz } from '../../../../../machine_learning/authz'; import type { IPrebuiltRuleAssetsClient } from '../../../../prebuilt_rules/logic/rule_assets/prebuilt_rule_assets_client'; import { applyRulePatch } from '../mergers/apply_rule_patch'; import { getIdError } from '../../../utils/utils'; +import { validateNonCustomizablePatchFields } from '../../../utils/validate'; import { convertAlertingRuleToRuleResponse } from '../converters/convert_alerting_rule_to_rule_response'; import { convertRuleResponseToAlertingRule } from '../converters/convert_rule_response_to_alerting_rule'; import { ClientError, toggleRuleEnabledOnUpdate, validateMlAuth } from '../utils'; @@ -51,6 +52,8 @@ export const patchRule = async ({ await validateMlAuth(mlAuthz, rulePatch.type ?? existingRule.type); + validateNonCustomizablePatchFields(rulePatch, existingRule); + const patchedRule = await applyRulePatch({ prebuiltRuleAssetClient, existingRule, diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_management/logic/detection_rules_client/methods/update_rule.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_management/logic/detection_rules_client/methods/update_rule.ts index cd84788026870..8fd7f7a89dcb7 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_management/logic/detection_rules_client/methods/update_rule.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_management/logic/detection_rules_client/methods/update_rule.ts @@ -11,6 +11,7 @@ import type { RuleResponse } from '../../../../../../../common/api/detection_eng import type { MlAuthz } from '../../../../../machine_learning/authz'; import { applyRuleUpdate } from '../mergers/apply_rule_update'; import { getIdError } from '../../../utils/utils'; +import { validateNonCustomizableUpdateFields } from '../../../utils/validate'; import { convertRuleResponseToAlertingRule } from '../converters/convert_rule_response_to_alerting_rule'; import { ClientError, toggleRuleEnabledOnUpdate, validateMlAuth } from '../utils'; @@ -50,6 +51,8 @@ export const updateRule = async ({ throw new ClientError(error.message, error.statusCode); } + validateNonCustomizableUpdateFields(ruleUpdate, existingRule); + const ruleWithUpdates = await applyRuleUpdate({ prebuiltRuleAssetClient, existingRule, diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_management/utils/validate.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_management/utils/validate.ts index 96aaef64b57c9..5ff9d2d97f2b0 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_management/utils/validate.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_management/utils/validate.ts @@ -6,38 +6,27 @@ */ import type { PartialRule } from '@kbn/alerting-plugin/server'; -import type { Rule } from '@kbn/alerting-plugin/common'; import { isEqual, xorWith } from 'lodash'; import { stringifyZodError } from '@kbn/zod-helpers'; -import type { - EqlRule, - EsqlRule, - NewTermsRule, - QueryRule, -} from '../../../../../common/api/detection_engine'; +import { shouldShowResponseActions } from '../../../../../common/detection_engine/utils'; import { type ResponseAction, type RuleCreateProps, RuleResponse, type RuleResponseAction, type RuleUpdateProps, + type RulePatchProps, } from '../../../../../common/api/detection_engine'; import { RESPONSE_ACTION_API_COMMAND_TO_CONSOLE_COMMAND_MAP, RESPONSE_CONSOLE_ACTION_COMMANDS_TO_REQUIRED_AUTHZ, } from '../../../../../common/endpoint/service/response_actions/constants'; -import { shouldShowResponseActions } from '../../../../../common/detection_engine/utils'; import type { SecuritySolutionApiRequestHandlerContext } from '../../../..'; import { CustomHttpRequestError } from '../../../../utils/custom_http_request_error'; -import type { EqlRuleParams, EsqlRuleParams, NewTermsRuleParams } from '../../rule_schema'; -import { - hasValidRuleType, - type RuleAlertType, - type RuleParams, - type UnifiedQueryRuleParams, -} from '../../rule_schema'; +import { hasValidRuleType, type RuleAlertType, type RuleParams } from '../../rule_schema'; import { type BulkError, createBulkErrorObject } from '../../routes/utils'; import { internalRuleToAPIResponse } from '../logic/detection_rules_client/converters/internal_rule_to_api_response'; +import { ClientError } from '../logic/detection_rules_client/utils'; export const transformValidateBulkError = ( ruleId: string, @@ -70,7 +59,13 @@ export const validateResponseActionsPermissions = async ( ruleUpdate: RuleCreateProps | RuleUpdateProps, existingRule?: RuleAlertType | null ): Promise => { - if (!shouldShowResponseActions(ruleUpdate.type)) { + const { experimentalFeatures } = await securitySolution.getConfig(); + if ( + !shouldShowResponseActions( + ruleUpdate.type, + experimentalFeatures.automatedResponseActionsForAllRulesEnabled + ) + ) { return; } @@ -117,14 +112,38 @@ export const validateResponseActionsPermissions = async ( }); }; -function rulePayloadContainsResponseActions( - rule: RuleCreateProps | RuleUpdateProps -): rule is QueryRule | EsqlRule | EqlRule | NewTermsRule { +function rulePayloadContainsResponseActions(rule: RuleCreateProps | RuleUpdateProps) { return 'response_actions' in rule; } -function ruleObjectContainsResponseActions( - rule?: RuleAlertType -): rule is Rule { +function ruleObjectContainsResponseActions(rule?: RuleAlertType) { return rule != null && 'params' in rule && 'responseActions' in rule?.params; } + +export const validateNonCustomizableUpdateFields = ( + ruleUpdate: RuleUpdateProps, + existingRule: RuleResponse +) => { + // We don't allow non-customizable fields to be changed for prebuilt rules + if (existingRule.rule_source && existingRule.rule_source.type === 'external') { + if (!isEqual(ruleUpdate.author, existingRule.author)) { + throw new ClientError(`Cannot update "author" field for prebuilt rules`, 400); + } else if (ruleUpdate.license !== existingRule.license) { + throw new ClientError(`Cannot update "license" field for prebuilt rules`, 400); + } + } +}; + +export const validateNonCustomizablePatchFields = ( + rulePatch: RulePatchProps, + existingRule: RuleResponse +) => { + // We don't allow non-customizable fields to be changed for prebuilt rules + if (existingRule.rule_source && existingRule.rule_source.type === 'external') { + if (rulePatch.author && !isEqual(rulePatch.author, existingRule.author)) { + throw new ClientError(`Cannot update "author" field for prebuilt rules`, 400); + } else if (rulePatch.license != null && rulePatch.license !== existingRule.license) { + throw new ClientError(`Cannot update "license" field for prebuilt rules`, 400); + } + } +}; diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_response_actions/schedule_notification_response_actions.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_response_actions/schedule_notification_response_actions.ts index b4f4689fed0ff..f3d9b42d24213 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_response_actions/schedule_notification_response_actions.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_response_actions/schedule_notification_response_actions.ts @@ -32,6 +32,9 @@ export const getScheduleNotificationResponseActionsService = const nestedAlerts = signals.map((signal) => expandDottedObject(signal as object)) as Alert[]; const alerts = nestedAlerts.filter((alert) => alert.agent?.id) as AlertWithAgent[]; + if (!alerts.length) { + return; + } return Promise.all( responseActions.map(async (responseAction) => { if ( diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_schema/model/rule_schemas.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_schema/model/rule_schemas.ts index e651ffeebaf49..c1192e9a75fd1 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_schema/model/rule_schemas.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_schema/model/rule_schemas.ts @@ -148,6 +148,7 @@ export const BaseRuleParams = z.object({ relatedIntegrations: RelatedIntegrationArray.optional(), requiredFields: RequiredFieldArray.optional(), setup: SetupGuide.optional(), + responseActions: z.array(RuleResponseAction).optional(), }); export type EqlSpecificRuleParams = z.infer; @@ -162,7 +163,6 @@ export const EqlSpecificRuleParams = z.object({ timestampField: TimestampField.optional(), tiebreakerField: TiebreakerField.optional(), alertSuppression: AlertSuppressionCamel.optional(), - responseActions: z.array(RuleResponseAction).optional(), }); export type EqlRuleParams = BaseRuleParams & EqlSpecificRuleParams; @@ -174,7 +174,6 @@ export const EsqlSpecificRuleParams = z.object({ language: z.literal('esql'), query: RuleQuery, alertSuppression: AlertSuppressionCamel.optional(), - responseActions: z.array(RuleResponseAction).optional(), }); export type EsqlRuleParams = BaseRuleParams & EsqlSpecificRuleParams; @@ -212,7 +211,6 @@ export const QuerySpecificRuleParams = z.object({ filters: RuleFilterArray.optional(), savedId: SavedQueryId.optional(), dataViewId: DataViewId.optional(), - responseActions: z.array(RuleResponseAction).optional(), alertSuppression: AlertSuppressionCamel.optional(), }); @@ -228,7 +226,6 @@ export const SavedQuerySpecificRuleParams = z.object({ query: RuleQuery.optional(), filters: RuleFilterArray.optional(), savedId: SavedQueryId, - responseActions: z.array(RuleResponseAction).optional(), alertSuppression: AlertSuppressionCamel.optional(), }); @@ -282,7 +279,6 @@ export const NewTermsSpecificRuleParams = z.object({ language: KqlQueryLanguage, dataViewId: DataViewId.optional(), alertSuppression: AlertSuppressionCamel.optional(), - responseActions: z.array(RuleResponseAction).optional(), }); export type NewTermsRuleParams = BaseRuleParams & NewTermsSpecificRuleParams; diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/eql/create_eql_alert_type.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/eql/create_eql_alert_type.ts index 9de8641d7b17c..12af1966b7dce 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/eql/create_eql_alert_type.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/eql/create_eql_alert_type.ts @@ -11,19 +11,14 @@ import { DEFAULT_APP_CATEGORIES } from '@kbn/core-application-common'; import { SERVER_APP_ID } from '../../../../../common/constants'; import { EqlRuleParams } from '../../rule_schema'; import { eqlExecutor } from './eql'; -import type { - CreateRuleOptions, - SecurityAlertType, - SignalSourceHit, - CreateRuleAdditionalOptions, -} from '../types'; +import type { CreateRuleOptions, SecurityAlertType, SignalSourceHit } from '../types'; import { validateIndexPatterns } from '../utils'; import type { BuildReasonMessage } from '../utils/reason_formatters'; import { wrapSuppressedAlerts } from '../utils/wrap_suppressed_alerts'; import { getIsAlertSuppressionActive } from '../utils/get_is_alert_suppression_active'; export const createEqlAlertType = ( - createOptions: CreateRuleOptions & CreateRuleAdditionalOptions + createOptions: CreateRuleOptions ): SecurityAlertType => { const { experimentalFeatures, version, licensing, scheduleNotificationResponseActionsService } = createOptions; diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/eql/eql.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/eql/eql.ts index 47e298392d7d9..cd8b76a93d23b 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/eql/eql.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/eql/eql.ts @@ -26,7 +26,7 @@ import type { SearchAfterAndBulkCreateReturnType, SignalSource, WrapSuppressedHits, - CreateRuleAdditionalOptions, + CreateRuleOptions, } from '../types'; import { addToSearchAfterReturn, @@ -71,7 +71,7 @@ interface EqlExecutorParams { isAlertSuppressionActive: boolean; experimentalFeatures: ExperimentalFeatures; state?: Record; - scheduleNotificationResponseActionsService: CreateRuleAdditionalOptions['scheduleNotificationResponseActionsService']; + scheduleNotificationResponseActionsService: CreateRuleOptions['scheduleNotificationResponseActionsService']; } export const eqlExecutor = async ({ @@ -104,7 +104,6 @@ export const eqlExecutor = async ({ const isLoggedRequestsEnabled = state?.isLoggedRequestsEnabled ?? false; const loggedRequests: RulePreviewLoggedRequest[] = []; - // eslint-disable-next-line complexity return withSecuritySpan('eqlExecutor', async () => { const result = createSearchAfterReturnType(); @@ -213,13 +212,11 @@ export const eqlExecutor = async ({ result.warningMessages.push(maxSignalsWarning); } - if (scheduleNotificationResponseActionsService) { - scheduleNotificationResponseActionsService({ - signals: result.createdSignals, - signalsCount: result.createdSignalsCount, - responseActions: completeRule.ruleParams.responseActions, - }); - } + scheduleNotificationResponseActionsService({ + signals: result.createdSignals, + signalsCount: result.createdSignalsCount, + responseActions: completeRule.ruleParams.responseActions, + }); return { result, ...(isLoggedRequestsEnabled ? { loggedRequests } : {}) }; } catch (error) { if ( diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/esql/create_esql_alert_type.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/esql/create_esql_alert_type.ts index 31afe8d2a191f..043b8e3b3a851 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/esql/create_esql_alert_type.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/esql/create_esql_alert_type.ts @@ -11,10 +11,10 @@ import { DEFAULT_APP_CATEGORIES } from '@kbn/core-application-common'; import { SERVER_APP_ID } from '../../../../../common/constants'; import { EsqlRuleParams } from '../../rule_schema'; import { esqlExecutor } from './esql'; -import type { CreateRuleOptions, SecurityAlertType, CreateRuleAdditionalOptions } from '../types'; +import type { CreateRuleOptions, SecurityAlertType } from '../types'; export const createEsqlAlertType = ( - createOptions: CreateRuleOptions & CreateRuleAdditionalOptions + createOptions: CreateRuleOptions ): SecurityAlertType => { const { version, experimentalFeatures, licensing, scheduleNotificationResponseActionsService } = createOptions; diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/esql/esql.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/esql/esql.ts index 1e5b1749e94f5..a076ea0c62635 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/esql/esql.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/esql/esql.ts @@ -28,7 +28,7 @@ import { rowToDocument } from './utils'; import { fetchSourceDocuments } from './fetch_source_documents'; import { buildReasonMessageForEsqlAlert } from '../utils/reason_formatters'; import type { RulePreviewLoggedRequest } from '../../../../../common/api/detection_engine/rule_preview/rule_preview.gen'; -import type { RunOpts, SignalSource, CreateRuleAdditionalOptions } from '../types'; +import type { CreateRuleOptions, RunOpts, SignalSource } from '../types'; import { logEsqlRequest } from '../utils/logged_requests'; import * as i18n from '../translations'; @@ -74,7 +74,7 @@ export const esqlExecutor = async ({ version: string; experimentalFeatures: ExperimentalFeatures; licensing: LicensingPluginSetup; - scheduleNotificationResponseActionsService: CreateRuleAdditionalOptions['scheduleNotificationResponseActionsService']; + scheduleNotificationResponseActionsService: CreateRuleOptions['scheduleNotificationResponseActionsService']; }) => { const loggedRequests: RulePreviewLoggedRequest[] = []; const ruleParams = completeRule.ruleParams; @@ -245,13 +245,11 @@ export const esqlExecutor = async ({ } } - if (scheduleNotificationResponseActionsService) { - scheduleNotificationResponseActionsService({ - signals: result.createdSignals, - signalsCount: result.createdSignalsCount, - responseActions: completeRule.ruleParams.responseActions, - }); - } + scheduleNotificationResponseActionsService({ + signals: result.createdSignals, + signalsCount: result.createdSignalsCount, + responseActions: completeRule.ruleParams.responseActions, + }); // no more results will be found if (response.values.length < size) { diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/indicator_match/create_indicator_match_alert_type.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/indicator_match/create_indicator_match_alert_type.ts index d7f3e96d9a43d..9c51d22d31ee1 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/indicator_match/create_indicator_match_alert_type.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/indicator_match/create_indicator_match_alert_type.ts @@ -20,7 +20,13 @@ import type { BuildReasonMessage } from '../utils/reason_formatters'; export const createIndicatorMatchAlertType = ( createOptions: CreateRuleOptions ): SecurityAlertType => { - const { eventsTelemetry, version, licensing, experimentalFeatures } = createOptions; + const { + eventsTelemetry, + version, + licensing, + experimentalFeatures, + scheduleNotificationResponseActionsService, + } = createOptions; return { id: INDICATOR_RULE_TYPE_ID, name: 'Indicator Match Rule', @@ -122,6 +128,7 @@ export const createIndicatorMatchAlertType = ( runOpts, licensing, experimentalFeatures, + scheduleNotificationResponseActionsService, }); return { ...result, state }; }, diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/indicator_match/indicator_match.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/indicator_match/indicator_match.ts index b8392a82bb6c0..d243943b9417c 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/indicator_match/indicator_match.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/indicator_match/indicator_match.ts @@ -16,7 +16,14 @@ import type { } from '@kbn/alerting-plugin/server'; import type { ListClient } from '@kbn/lists-plugin/server'; import type { Filter } from '@kbn/es-query'; -import type { RuleRangeTuple, BulkCreate, WrapHits, WrapSuppressedHits, RunOpts } from '../types'; +import type { + RuleRangeTuple, + BulkCreate, + WrapHits, + WrapSuppressedHits, + RunOpts, + CreateRuleOptions, +} from '../types'; import type { ITelemetryEventsSender } from '../../../telemetry/sender'; import { createThreatSignals } from './threat_mapping/create_threat_signals'; import type { CompleteRule, ThreatRuleParams } from '../../rule_schema'; @@ -47,6 +54,7 @@ export const indicatorMatchExecutor = async ({ runOpts, licensing, experimentalFeatures, + scheduleNotificationResponseActionsService, }: { inputIndex: string[]; runtimeMappings: estypes.MappingRuntimeFields | undefined; @@ -67,6 +75,7 @@ export const indicatorMatchExecutor = async ({ wrapSuppressedHits: WrapSuppressedHits; runOpts: RunOpts; licensing: LicensingPluginSetup; + scheduleNotificationResponseActionsService: CreateRuleOptions['scheduleNotificationResponseActionsService']; experimentalFeatures: ExperimentalFeatures; }) => { const ruleParams = completeRule.ruleParams; @@ -107,6 +116,7 @@ export const indicatorMatchExecutor = async ({ runOpts, licensing, experimentalFeatures, + scheduleNotificationResponseActionsService, }); }); }; diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/indicator_match/threat_mapping/create_threat_signals.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/indicator_match/threat_mapping/create_threat_signals.ts index 4d477d53604a4..f05914201ad09 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/indicator_match/threat_mapping/create_threat_signals.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/indicator_match/threat_mapping/create_threat_signals.ts @@ -74,6 +74,7 @@ export const createThreatSignals = async ({ unprocessedExceptions, licensing, experimentalFeatures, + scheduleNotificationResponseActionsService, }: CreateThreatSignalsOptions): Promise => { const threatMatchedFields = getMatchedFields(threatMapping); const threatFieldsLength = threatMatchedFields.threat.length; @@ -460,7 +461,11 @@ export const createThreatSignals = async ({ `Error trying to close point in time: "${threatPitId}", it will expire within "${THREAT_PIT_KEEP_ALIVE}". Error is: "${error}"` ); } - + scheduleNotificationResponseActionsService({ + signals: results.createdSignals, + signalsCount: results.createdSignalsCount, + responseActions: completeRule.ruleParams.responseActions, + }); ruleExecutionLogger.debug('Indicator matching rule has completed'); return results; }; diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/indicator_match/threat_mapping/types.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/indicator_match/threat_mapping/types.ts index 37bc9d1810137..4eac8bd6a8864 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/indicator_match/threat_mapping/types.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/indicator_match/threat_mapping/types.ts @@ -38,6 +38,7 @@ import type { WrapSuppressedHits, OverrideBodyQuery, RunOpts, + CreateRuleOptions, } from '../../types'; import type { CompleteRule, ThreatRuleParams } from '../../../rule_schema'; import type { IRuleExecutionLogForExecutors } from '../../../rule_monitoring'; @@ -80,6 +81,7 @@ export interface CreateThreatSignalsOptions { runOpts: RunOpts; licensing: LicensingPluginSetup; experimentalFeatures: ExperimentalFeatures; + scheduleNotificationResponseActionsService: CreateRuleOptions['scheduleNotificationResponseActionsService']; } export interface CreateThreatSignalOptions { @@ -172,6 +174,7 @@ export interface CreateEventSignalOptions { } type EntryKey = 'field' | 'value'; + export interface BuildThreatMappingFilterOptions { chunkSize?: number; threatList: ThreatListItem[]; @@ -273,6 +276,7 @@ interface BaseThreatNamedQuery { value: string; queryType: string; } + export interface ThreatMatchNamedQuery extends BaseThreatNamedQuery { id: string; index: string; @@ -325,6 +329,7 @@ export interface EventDoc { } export type EventItem = estypes.SearchHit; + export interface EventCountOptions { esClient: ElasticsearchClient; index: string[]; diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/ml/create_ml_alert_type.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/ml/create_ml_alert_type.ts index 09a4a2e4cb1ee..4d896c4efdaa4 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/ml/create_ml_alert_type.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/ml/create_ml_alert_type.ts @@ -19,7 +19,8 @@ import { wrapSuppressedAlerts } from '../utils/wrap_suppressed_alerts'; export const createMlAlertType = ( createOptions: CreateRuleOptions ): SecurityAlertType => { - const { experimentalFeatures, ml, licensing } = createOptions; + const { experimentalFeatures, ml, licensing, scheduleNotificationResponseActionsService } = + createOptions; return { id: ML_RULE_TYPE_ID, name: 'Machine Learning Rule', @@ -106,6 +107,7 @@ export const createMlAlertType = ( alertWithSuppression, isAlertSuppressionActive, experimentalFeatures, + scheduleNotificationResponseActionsService, }); return { ...result, state }; }, diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/ml/ml.test.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/ml/ml.test.ts index 59a0204ef9545..2a3fa8360e3f8 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/ml/ml.test.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/ml/ml.test.ts @@ -23,6 +23,7 @@ jest.mock('./bulk_create_ml_signals'); describe('ml_executor', () => { let mockExperimentalFeatures: jest.Mocked; + let mockScheduledNotificationResponseAction: jest.Mock; let jobsSummaryMock: jest.Mock; let forceStartDatafeedsMock: jest.Mock; let stopDatafeedsMock: jest.Mock; @@ -40,6 +41,7 @@ describe('ml_executor', () => { beforeEach(() => { mockExperimentalFeatures = {} as jest.Mocked; + mockScheduledNotificationResponseAction = jest.fn(); jobsSummaryMock = jest.fn(); mlMock = mlPluginServerMock.createSetupContract(); mlMock.jobServiceProvider.mockReturnValue({ @@ -88,6 +90,7 @@ describe('ml_executor', () => { alertWithSuppression: jest.fn(), isAlertSuppressionActive: true, experimentalFeatures: mockExperimentalFeatures, + scheduleNotificationResponseActionsService: mockScheduledNotificationResponseAction, }) ).rejects.toThrow('ML plugin unavailable during rule execution'); }); @@ -110,6 +113,7 @@ describe('ml_executor', () => { alertWithSuppression: jest.fn(), isAlertSuppressionActive: true, experimentalFeatures: mockExperimentalFeatures, + scheduleNotificationResponseActionsService: mockScheduledNotificationResponseAction, }); expect(ruleExecutionLogger.warn).toHaveBeenCalled(); expect(ruleExecutionLogger.warn.mock.calls[0][0]).toContain( @@ -143,6 +147,7 @@ describe('ml_executor', () => { alertWithSuppression: jest.fn(), isAlertSuppressionActive: true, experimentalFeatures: mockExperimentalFeatures, + scheduleNotificationResponseActionsService: mockScheduledNotificationResponseAction, }); expect(ruleExecutionLogger.warn).toHaveBeenCalled(); expect(ruleExecutionLogger.warn.mock.calls[0][0]).toContain( @@ -172,6 +177,7 @@ describe('ml_executor', () => { alertWithSuppression: jest.fn(), isAlertSuppressionActive: true, experimentalFeatures: mockExperimentalFeatures, + scheduleNotificationResponseActionsService: mockScheduledNotificationResponseAction, }); expect(result.userError).toEqual(true); expect(result.success).toEqual(false); @@ -204,6 +210,7 @@ describe('ml_executor', () => { alertWithSuppression: jest.fn(), isAlertSuppressionActive: true, experimentalFeatures: mockExperimentalFeatures, + scheduleNotificationResponseActionsService: mockScheduledNotificationResponseAction, }); expect(result).toEqual( @@ -212,4 +219,29 @@ describe('ml_executor', () => { }) ); }); + it('should call scheduleNotificationResponseActionsService', async () => { + const result = await mlExecutor({ + completeRule: mlCompleteRule, + tuple, + ml: mlMock, + services: alertServices, + ruleExecutionLogger, + listClient, + bulkCreate: jest.fn(), + wrapHits: jest.fn(), + exceptionFilter: undefined, + unprocessedExceptions: [], + wrapSuppressedHits: jest.fn(), + alertTimestampOverride: undefined, + alertWithSuppression: jest.fn(), + isAlertSuppressionActive: true, + experimentalFeatures: mockExperimentalFeatures, + scheduleNotificationResponseActionsService: mockScheduledNotificationResponseAction, + }); + expect(mockScheduledNotificationResponseAction).toBeCalledWith({ + signals: result.createdSignals, + signalsCount: result.createdSignalsCount, + responseActions: mlCompleteRule.ruleParams.responseActions, + }); + }); }); diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/ml/ml.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/ml/ml.ts index 4b7de9b27a667..1da14640c5a51 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/ml/ml.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/ml/ml.ts @@ -23,7 +23,13 @@ import type { CompleteRule, MachineLearningRuleParams } from '../../rule_schema' import { bulkCreateMlSignals } from './bulk_create_ml_signals'; import { filterEventsAgainstList } from '../utils/large_list_filters/filter_events_against_list'; import { findMlSignals } from './find_ml_signals'; -import type { BulkCreate, RuleRangeTuple, WrapHits, WrapSuppressedHits } from '../types'; +import type { + BulkCreate, + CreateRuleOptions, + RuleRangeTuple, + WrapHits, + WrapSuppressedHits, +} from '../types'; import { addToSearchAfterReturn, createErrorsFromShard, @@ -54,6 +60,7 @@ interface MachineLearningRuleExecutorParams { alertWithSuppression: SuppressedAlertService; isAlertSuppressionActive: boolean; experimentalFeatures: ExperimentalFeatures; + scheduleNotificationResponseActionsService: CreateRuleOptions['scheduleNotificationResponseActionsService']; } export const mlExecutor = async ({ @@ -72,6 +79,7 @@ export const mlExecutor = async ({ alertTimestampOverride, alertWithSuppression, experimentalFeatures, + scheduleNotificationResponseActionsService, }: MachineLearningRuleExecutorParams) => { const result = createSearchAfterReturnType(); const ruleParams = completeRule.ruleParams; @@ -191,6 +199,11 @@ export const mlExecutor = async ({ const searchErrors = createErrorsFromShard({ errors: shardFailures, }); + scheduleNotificationResponseActionsService({ + signals: result.createdSignals, + signalsCount: result.createdSignalsCount, + responseActions: completeRule.ruleParams.responseActions, + }); return mergeReturns([ result, createSearchAfterReturnType({ diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/new_terms/create_new_terms_alert_type.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/new_terms/create_new_terms_alert_type.ts index fc0c4b31426df..6b50f0fe0505e 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/new_terms/create_new_terms_alert_type.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/new_terms/create_new_terms_alert_type.ts @@ -12,7 +12,7 @@ import { DEFAULT_APP_CATEGORIES } from '@kbn/core-application-common'; import { SERVER_APP_ID } from '../../../../../common/constants'; import { NewTermsRuleParams } from '../../rule_schema'; -import type { CreateRuleOptions, SecurityAlertType, CreateRuleAdditionalOptions } from '../types'; +import type { CreateRuleOptions, SecurityAlertType } from '../types'; import { singleSearchAfter } from '../utils/single_search_after'; import { getFilter } from '../utils/get_filter'; import { wrapNewTermsAlerts } from './wrap_new_terms_alerts'; @@ -46,7 +46,7 @@ import { multiTermsComposite } from './multi_terms_composite'; import type { GenericBulkCreateResponse } from '../utils/bulk_create_with_suppression'; export const createNewTermsAlertType = ( - createOptions: CreateRuleOptions & CreateRuleAdditionalOptions + createOptions: CreateRuleOptions ): SecurityAlertType => { const { logger, licensing, experimentalFeatures, scheduleNotificationResponseActionsService } = createOptions; @@ -415,13 +415,11 @@ export const createNewTermsAlertType = ( afterKey = searchResultWithAggs.aggregations.new_terms.after_key; } - if (scheduleNotificationResponseActionsService) { - scheduleNotificationResponseActionsService({ - signals: result.createdSignals, - signalsCount: result.createdSignalsCount, - responseActions: completeRule.ruleParams.responseActions, - }); - } + scheduleNotificationResponseActionsService({ + signals: result.createdSignals, + signalsCount: result.createdSignalsCount, + responseActions: completeRule.ruleParams.responseActions, + }); return { ...result, state }; }, diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/query/query.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/query/query.ts index edf7ece7cc84b..8c235c5e8f238 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/query/query.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/query/query.ts @@ -22,7 +22,7 @@ import type { UnifiedQueryRuleParams } from '../../rule_schema'; import type { ExperimentalFeatures } from '../../../../../common/experimental_features'; import { buildReasonMessageForQueryAlert } from '../utils/reason_formatters'; import { withSecuritySpan } from '../../../../utils/with_security_span'; -import type { CreateRuleAdditionalOptions, RunOpts } from '../types'; +import type { CreateRuleOptions, RunOpts } from '../types'; export const queryExecutor = async ({ runOpts, @@ -42,7 +42,7 @@ export const queryExecutor = async ({ version: string; spaceId: string; bucketHistory?: BucketHistory[]; - scheduleNotificationResponseActionsService: CreateRuleAdditionalOptions['scheduleNotificationResponseActionsService']; + scheduleNotificationResponseActionsService: CreateRuleOptions['scheduleNotificationResponseActionsService']; licensing: LicensingPluginSetup; }) => { const completeRule = runOpts.completeRule; @@ -98,13 +98,11 @@ export const queryExecutor = async ({ state: {}, }; - if (scheduleNotificationResponseActionsService) { - scheduleNotificationResponseActionsService({ - signals: result.createdSignals, - signalsCount: result.createdSignalsCount, - responseActions: completeRule.ruleParams.responseActions, - }); - } + scheduleNotificationResponseActionsService({ + signals: result.createdSignals, + signalsCount: result.createdSignalsCount, + responseActions: completeRule.ruleParams.responseActions, + }); return result; }); diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/threshold/create_threshold_alert_type.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/threshold/create_threshold_alert_type.ts index f48cea676b953..a890315aa2688 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/threshold/create_threshold_alert_type.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/threshold/create_threshold_alert_type.ts @@ -19,7 +19,8 @@ import { validateIndexPatterns } from '../utils'; export const createThresholdAlertType = ( createOptions: CreateRuleOptions ): SecurityAlertType => { - const { version, licensing, experimentalFeatures } = createOptions; + const { version, licensing, experimentalFeatures, scheduleNotificationResponseActionsService } = + createOptions; return { id: THRESHOLD_RULE_TYPE_ID, name: 'Threshold Rule', @@ -102,6 +103,7 @@ export const createThresholdAlertType = ( runOpts: execOptions.runOpts, licensing, experimentalFeatures, + scheduleNotificationResponseActionsService, }); return result; }, diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/threshold/threshold.test.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/threshold/threshold.test.ts index 8c790596b99ba..de4af3354794d 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/threshold/threshold.test.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/threshold/threshold.test.ts @@ -27,7 +27,7 @@ jest.mock('../utils/get_filter', () => ({ getFilter: jest.fn() })); describe('threshold_executor', () => { let alertServices: RuleExecutorServicesMock; let ruleExecutionLogger: ReturnType; - + let mockScheduledNotificationResponseAction: jest.Mock; const version = '8.0.0'; const params = getThresholdRuleParams(); const thresholdCompleteRule = getCompleteRuleMock(params); @@ -54,6 +54,7 @@ describe('threshold_executor', () => { ruleName: thresholdCompleteRule.ruleConfig.name, ruleType: thresholdCompleteRule.ruleConfig.ruleTypeId, }); + mockScheduledNotificationResponseAction = jest.fn(); }); describe('thresholdExecutor', () => { @@ -113,6 +114,7 @@ describe('threshold_executor', () => { runOpts: {} as RunOpts, licensing, experimentalFeatures: {} as ExperimentalFeatures, + scheduleNotificationResponseActionsService: mockScheduledNotificationResponseAction, }); expect(response.state).toEqual({ initialized: true, @@ -178,6 +180,7 @@ describe('threshold_executor', () => { runOpts: {} as RunOpts, licensing, experimentalFeatures: {} as ExperimentalFeatures, + scheduleNotificationResponseActionsService: mockScheduledNotificationResponseAction, }); expect(result.warningMessages).toEqual([ `The following exceptions won't be applied to rule execution: ${ @@ -185,5 +188,46 @@ describe('threshold_executor', () => { }`, ]); }); + it('should call scheduleNotificationResponseActionsService', async () => { + const ruleDataClientMock = createRuleDataClientMock(); + const state = { + initialized: true, + signalHistory: {}, + }; + const result = await thresholdExecutor({ + completeRule: thresholdCompleteRule, + tuple, + services: alertServices, + state, + version, + ruleExecutionLogger, + startedAt: new Date(), + bulkCreate: jest.fn().mockImplementation((hits) => ({ + errors: [], + success: true, + bulkCreateDuration: '0', + createdItemsCount: 0, + createdItems: [], + })), + wrapHits: jest.fn(), + ruleDataClient: ruleDataClientMock, + runtimeMappings: {}, + inputIndex: ['auditbeat-*'], + primaryTimestamp: TIMESTAMP, + aggregatableTimestampField: TIMESTAMP, + exceptionFilter: undefined, + unprocessedExceptions: [getExceptionListItemSchemaMock()], + spaceId: 'default', + runOpts: {} as RunOpts, + licensing, + experimentalFeatures: {} as ExperimentalFeatures, + scheduleNotificationResponseActionsService: mockScheduledNotificationResponseAction, + }); + expect(mockScheduledNotificationResponseAction).toBeCalledWith({ + signals: result.createdSignals, + signalsCount: result.createdSignalsCount, + responseActions: thresholdCompleteRule.ruleParams.responseActions, + }); + }); }); }); diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/threshold/threshold.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/threshold/threshold.ts index 06a0ff89ccc40..d56e164438509 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/threshold/threshold.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/threshold/threshold.ts @@ -33,6 +33,7 @@ import type { SearchAfterAndBulkCreateReturnType, WrapHits, RunOpts, + CreateRuleOptions, } from '../types'; import type { ThresholdAlertState, ThresholdSignalHistory } from './types'; import { @@ -68,6 +69,7 @@ export const thresholdExecutor = async ({ runOpts, licensing, experimentalFeatures, + scheduleNotificationResponseActionsService, }: { inputIndex: string[]; runtimeMappings: estypes.MappingRuntimeFields | undefined; @@ -90,6 +92,7 @@ export const thresholdExecutor = async ({ runOpts: RunOpts; licensing: LicensingPluginSetup; experimentalFeatures: ExperimentalFeatures; + scheduleNotificationResponseActionsService: CreateRuleOptions['scheduleNotificationResponseActionsService']; }): Promise => { const result = createSearchAfterReturnType(); const ruleParams = completeRule.ruleParams; @@ -209,7 +212,11 @@ export const thresholdExecutor = async ({ result.errors.push(...searchErrors); result.warningMessages.push(...warnings); result.searchAfterTimes = searchDurations; - + scheduleNotificationResponseActionsService({ + signals: result.createdSignals, + signalsCount: result.createdSignalsCount, + responseActions: completeRule.ruleParams.responseActions, + }); return { ...result, state: { diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/types.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/types.ts index 6e2999ae5e3b2..34307ea495268 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/types.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/types.ts @@ -163,6 +163,7 @@ export interface CreateRuleOptions { eventsTelemetry?: ITelemetryEventsSender | undefined; version: string; licensing: LicensingPluginSetup; + scheduleNotificationResponseActionsService: (params: ScheduleNotificationActions) => void; } export interface ScheduleNotificationActions { @@ -171,11 +172,7 @@ export interface ScheduleNotificationActions { responseActions: RuleResponseAction[] | undefined; } -export interface CreateRuleAdditionalOptions { - scheduleNotificationResponseActionsService?: (params: ScheduleNotificationActions) => void; -} - -export interface CreateQueryRuleOptions extends CreateRuleOptions, CreateRuleAdditionalOptions { +export interface CreateQueryRuleOptions extends CreateRuleOptions { id: typeof QUERY_RULE_TYPE_ID | typeof SAVED_QUERY_RULE_TYPE_ID; name: 'Custom Query Rule' | 'Saved Query Rule'; } diff --git a/x-pack/plugins/security_solution/server/lib/entity_analytics/entity_store/definition.ts b/x-pack/plugins/security_solution/server/lib/entity_analytics/entity_store/definition.ts index a72e00bf7aceb..09dea151a050a 100644 --- a/x-pack/plugins/security_solution/server/lib/entity_analytics/entity_store/definition.ts +++ b/x-pack/plugins/security_solution/server/lib/entity_analytics/entity_store/definition.ts @@ -27,9 +27,8 @@ export const buildHostEntityDefinition = (space: string): EntityDefinition => 'host.type', 'host.architecture', ], - history: { + latest: { timestampField: '@timestamp', - interval: '1m', }, version: '1.0.0', managed: true, @@ -44,9 +43,8 @@ export const buildUserEntityDefinition = (space: string): EntityDefinition => identityFields: ['user.name'], displayNameTemplate: '{{user.name}}', metadata: ['user.email', 'user.full_name', 'user.hash', 'user.id', 'user.name', 'user.roles'], - history: { + latest: { timestampField: '@timestamp', - interval: '1m', }, version: '1.0.0', managed: true, diff --git a/x-pack/plugins/security_solution/server/plugin.ts b/x-pack/plugins/security_solution/server/plugin.ts index 5ea5c7ba5cd9e..1f144f8189ed7 100644 --- a/x-pack/plugins/security_solution/server/plugin.ts +++ b/x-pack/plugins/security_solution/server/plugin.ts @@ -76,10 +76,7 @@ import { PolicyWatcher } from './endpoint/lib/policy/license_watch'; import previewPolicy from './lib/detection_engine/routes/index/preview_policy.json'; import type { IRuleMonitoringService } from './lib/detection_engine/rule_monitoring'; import { createRuleMonitoringService } from './lib/detection_engine/rule_monitoring'; -import type { - CreateRuleAdditionalOptions, - CreateRuleOptions, -} from './lib/detection_engine/rule_types/types'; +import type { CreateRuleOptions } from './lib/detection_engine/rule_types/types'; // eslint-disable-next-line no-restricted-imports import { isLegacyNotificationRuleExecutor, @@ -280,6 +277,10 @@ export class Plugin implements ISecuritySolutionPlugin { eventsTelemetry: this.telemetryEventsSender, version: pluginContext.env.packageInfo.version, licensing: plugins.licensing, + scheduleNotificationResponseActionsService: getScheduleNotificationResponseActionsService({ + endpointAppContextService: this.endpointAppContextService, + osqueryCreateActionService: plugins.osquery.createActionService, + }), }; const ruleDataServiceOptions = { @@ -321,28 +322,18 @@ export class Plugin implements ISecuritySolutionPlugin { analytics: core.analytics, }; - const ruleAdditionalOptions: CreateRuleAdditionalOptions = { - scheduleNotificationResponseActionsService: getScheduleNotificationResponseActionsService({ - endpointAppContextService: this.endpointAppContextService, - osqueryCreateActionService: plugins.osquery.createActionService, - }), - }; - const securityRuleTypeWrapper = createSecurityRuleTypeWrapper(securityRuleTypeOptions); - plugins.alerting.registerType( - securityRuleTypeWrapper(createEqlAlertType({ ...ruleOptions, ...ruleAdditionalOptions })) - ); + plugins.alerting.registerType(securityRuleTypeWrapper(createEqlAlertType({ ...ruleOptions }))); if (!experimentalFeatures.esqlRulesDisabled) { plugins.alerting.registerType( - securityRuleTypeWrapper(createEsqlAlertType({ ...ruleOptions, ...ruleAdditionalOptions })) + securityRuleTypeWrapper(createEsqlAlertType({ ...ruleOptions })) ); } plugins.alerting.registerType( securityRuleTypeWrapper( createQueryAlertType({ ...ruleOptions, - ...ruleAdditionalOptions, id: SAVED_QUERY_RULE_TYPE_ID, name: 'Saved Query Rule', }) @@ -356,7 +347,6 @@ export class Plugin implements ISecuritySolutionPlugin { securityRuleTypeWrapper( createQueryAlertType({ ...ruleOptions, - ...ruleAdditionalOptions, id: QUERY_RULE_TYPE_ID, name: 'Custom Query Rule', }) @@ -364,7 +354,7 @@ export class Plugin implements ISecuritySolutionPlugin { ); plugins.alerting.registerType(securityRuleTypeWrapper(createThresholdAlertType(ruleOptions))); plugins.alerting.registerType( - securityRuleTypeWrapper(createNewTermsAlertType({ ...ruleOptions, ...ruleAdditionalOptions })) + securityRuleTypeWrapper(createNewTermsAlertType({ ...ruleOptions })) ); // TODO We need to get the endpoint routes inside of initRoutes @@ -546,8 +536,6 @@ export class Plugin implements ISecuritySolutionPlugin { plugins.elasticAssistant.registerTools( APP_UI_ID, getAssistantTools({ - naturalLanguageESQLToolEnabled: - config.experimentalFeatures.assistantNaturalLanguageESQLTool, assistantKnowledgeBaseByDefault: config.experimentalFeatures.assistantKnowledgeBaseByDefault, }) diff --git a/x-pack/plugins/security_solution_serverless/server/common/services/usage_reporting_service.test.ts b/x-pack/plugins/security_solution_serverless/server/common/services/usage_reporting_service.test.ts new file mode 100644 index 0000000000000..e43df68cc200b --- /dev/null +++ b/x-pack/plugins/security_solution_serverless/server/common/services/usage_reporting_service.test.ts @@ -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 fetch from 'node-fetch'; +import https from 'https'; +import { merge } from 'lodash'; + +import { KBN_CERT_PATH, KBN_KEY_PATH, CA_CERT_PATH } from '@kbn/dev-utils'; + +import type { UsageApiConfigSchema } from '../../config'; +import type { UsageRecord } from '../../types'; + +import { UsageReportingService } from './usage_reporting_service'; +import { USAGE_REPORTING_ENDPOINT, USAGE_SERVICE_USAGE_URL } from '../../constants'; + +jest.mock('node-fetch'); +const { Response } = jest.requireActual('node-fetch'); + +describe('UsageReportingService', () => { + let usageApiConfig: UsageApiConfigSchema; + let service: UsageReportingService; + + function generateUsageApiConfig(overrides?: Partial): UsageApiConfigSchema { + const DEFAULT_USAGE_API_CONFIG = { enabled: false }; + usageApiConfig = merge(DEFAULT_USAGE_API_CONFIG, overrides); + + return usageApiConfig; + } + + function setupService( + usageApi: UsageApiConfigSchema = generateUsageApiConfig() + ): UsageReportingService { + service = new UsageReportingService(usageApi); + return service; + } + + function generateUsageRecord(overrides?: Partial): UsageRecord { + const date = new Date().toISOString(); + const DEFAULT_USAGE_RECORD = { + id: `usage-record-id-${date}`, + usage_timestamp: date, + creation_timestamp: date, + usage: {}, + source: {}, + } as UsageRecord; + return merge(DEFAULT_USAGE_RECORD, overrides); + } + + afterEach(() => { + jest.clearAllMocks(); + }); + + describe('usageApi configs not provided', () => { + beforeEach(() => { + setupService(); + }); + + it('should still work if usageApi.url is not provided', async () => { + const usageRecord = generateUsageRecord(); + const records: UsageRecord[] = [usageRecord]; + const mockResponse = new Response(null, { status: 200 }); + (fetch as jest.MockedFunction).mockResolvedValueOnce(mockResponse); + + const response = await service.reportUsage(records); + + expect(fetch).toHaveBeenCalledTimes(1); + expect(fetch).toHaveBeenCalledWith(USAGE_SERVICE_USAGE_URL, { + method: 'post', + body: JSON.stringify(records), + headers: { 'Content-Type': 'application/json' }, + agent: expect.any(https.Agent), + }); + expect(response).toBe(mockResponse); + }); + + it('should use an agent with rejectUnauthorized false if config.enabled is false', async () => { + const usageRecord = generateUsageRecord(); + const records: UsageRecord[] = [usageRecord]; + const mockResponse = new Response(null, { status: 200 }); + (fetch as jest.MockedFunction).mockResolvedValueOnce(mockResponse); + + const response = await service.reportUsage(records); + + expect(fetch).toHaveBeenCalledTimes(1); + expect(fetch).toHaveBeenCalledWith(USAGE_SERVICE_USAGE_URL, { + method: 'post', + body: JSON.stringify(records), + headers: { 'Content-Type': 'application/json' }, + agent: expect.objectContaining({ + options: expect.objectContaining({ rejectUnauthorized: false }), + }), + }); + expect(response).toBe(mockResponse); + }); + + it('should not set agent if the URL is not https', async () => { + const url = 'http://usage-api.example'; + setupService(generateUsageApiConfig({ url })); + const usageRecord = generateUsageRecord(); + const records: UsageRecord[] = [usageRecord]; + const mockResponse = new Response(null, { status: 200 }); + (fetch as jest.MockedFunction).mockResolvedValue(mockResponse); + + const response = await service.reportUsage(records); + + expect(fetch).toHaveBeenCalledTimes(1); + expect(fetch).toHaveBeenCalledWith(`${url}${USAGE_REPORTING_ENDPOINT}`, { + method: 'post', + body: JSON.stringify(records), + headers: { 'Content-Type': 'application/json' }, + }); + expect(response).toBe(mockResponse); + }); + }); + + describe('usageApi configs provided', () => { + const DEFAULT_CONFIG = { + enabled: true, + url: 'https://usage-api.example', + tls: { + certificate: KBN_CERT_PATH, + key: KBN_KEY_PATH, + ca: CA_CERT_PATH, + }, + }; + + beforeEach(() => { + setupService(generateUsageApiConfig(DEFAULT_CONFIG)); + }); + + it('should use usageApi.url if provided', async () => { + const usageRecord = generateUsageRecord(); + const records: UsageRecord[] = [usageRecord]; + const mockResponse = new Response(null, { status: 200 }); + (fetch as jest.MockedFunction).mockResolvedValueOnce(mockResponse); + + const response = await service.reportUsage(records); + const url = `${DEFAULT_CONFIG.url}${USAGE_REPORTING_ENDPOINT}`; + + expect(fetch).toHaveBeenCalledTimes(1); + expect(fetch).toHaveBeenCalledWith(url, { + method: 'post', + body: JSON.stringify(records), + headers: { 'Content-Type': 'application/json' }, + agent: expect.any(https.Agent), + }); + expect(response).toBe(mockResponse); + }); + + it('should use an agent with TLS configuration if config.enabled is true', async () => { + const usageRecord = generateUsageRecord(); + const records: UsageRecord[] = [usageRecord]; + const mockResponse = new Response(null, { status: 200 }); + (fetch as jest.MockedFunction).mockResolvedValueOnce(mockResponse); + + const response = await service.reportUsage(records); + const url = `${DEFAULT_CONFIG.url}${USAGE_REPORTING_ENDPOINT}`; + + expect(fetch).toHaveBeenCalledTimes(1); + expect(fetch).toHaveBeenCalledWith(url, { + method: 'post', + body: JSON.stringify(records), + headers: { 'Content-Type': 'application/json' }, + agent: expect.objectContaining({ + options: expect.objectContaining({ + cert: expect.any(String), + key: expect.any(String), + ca: expect.arrayContaining([expect.any(String)]), + }), + }), + }); + expect(response).toBe(mockResponse); + }); + }); +}); diff --git a/x-pack/plugins/security_solution_serverless/server/common/services/usage_reporting_service.ts b/x-pack/plugins/security_solution_serverless/server/common/services/usage_reporting_service.ts index 0e47b982e692e..ee402872ef33a 100644 --- a/x-pack/plugins/security_solution_serverless/server/common/services/usage_reporting_service.ts +++ b/x-pack/plugins/security_solution_serverless/server/common/services/usage_reporting_service.ts @@ -5,29 +5,77 @@ * 2.0. */ -import type { Response } from 'node-fetch'; +import type { RequestInit, Response } from 'node-fetch'; + import fetch from 'node-fetch'; import https from 'https'; -import { USAGE_SERVICE_USAGE_URL } from '../../constants'; +import { SslConfig, sslSchema } from '@kbn/server-http-tools'; + import type { UsageRecord } from '../../types'; +import type { UsageApiConfigSchema, TlsConfigSchema } from '../../config'; + +import { USAGE_REPORTING_ENDPOINT, USAGE_SERVICE_USAGE_URL } from '../../constants'; -// TODO remove once we have the CA available -const agent = new https.Agent({ rejectUnauthorized: false }); export class UsageReportingService { - public async reportUsage( - records: UsageRecord[], - url = USAGE_SERVICE_USAGE_URL - ): Promise { - const isHttps = url.includes('https'); + private agent: https.Agent | undefined; - return fetch(url, { + constructor(private readonly config: UsageApiConfigSchema) {} + + public async reportUsage(records: UsageRecord[]): Promise { + const reqArgs: RequestInit = { method: 'post', body: JSON.stringify(records), headers: { 'Content-Type': 'application/json' }, - agent: isHttps ? agent : undefined, // Conditionally add agent if URL is HTTPS for supporting integration tests. + }; + if (this.usageApiUrl.includes('https')) { + reqArgs.agent = this.httpAgent; + } + return fetch(this.usageApiUrl, reqArgs); + } + + private get tlsConfigs(): NonNullable { + if (!this.config.tls) { + throw new Error('UsageReportingService: usageApi.tls configs not provided'); + } + + return this.config.tls; + } + + private get usageApiUrl(): string { + if (!this.config.url) { + return USAGE_SERVICE_USAGE_URL; + } + + return `${this.config.url}${USAGE_REPORTING_ENDPOINT}`; + } + + private get httpAgent(): https.Agent { + if (this.agent) { + return this.agent; + } + + if (!this.config.enabled) { + this.agent = new https.Agent({ rejectUnauthorized: false }); + return this.agent; + } + + const tlsConfig = new SslConfig( + sslSchema.validate({ + enabled: true, + certificate: this.tlsConfigs.certificate, + key: this.tlsConfigs.key, + certificateAuthorities: this.tlsConfigs.ca, + }) + ); + + this.agent = new https.Agent({ + rejectUnauthorized: tlsConfig.rejectUnauthorized, + cert: tlsConfig.certificate, + key: tlsConfig.key, + ca: tlsConfig.certificateAuthorities, }); + + return this.agent; } } - -export const usageReportingService = new UsageReportingService(); diff --git a/x-pack/plugins/security_solution_serverless/server/config.ts b/x-pack/plugins/security_solution_serverless/server/config.ts index 96e743a59b425..d4bafd9b9ddb9 100644 --- a/x-pack/plugins/security_solution_serverless/server/config.ts +++ b/x-pack/plugins/security_solution_serverless/server/config.ts @@ -16,19 +16,19 @@ import type { ExperimentalFeatures } from '../common/experimental_features'; import { productTypes } from '../common/config'; import { parseExperimentalConfigValue } from '../common/experimental_features'; -const usageApiConfig = schema.maybe( - schema.object({ - enabled: schema.maybe(schema.boolean()), - url: schema.string(), - tls: schema.maybe( - schema.object({ - certificate: schema.string(), - key: schema.string(), - ca: schema.string(), - }) - ), - }) -); +const tlsConfig = schema.object({ + certificate: schema.string(), + key: schema.string(), + ca: schema.string(), +}); +export type TlsConfigSchema = TypeOf; + +const usageApiConfig = schema.object({ + enabled: schema.boolean({ defaultValue: false }), + url: schema.maybe(schema.string()), + tls: schema.maybe(tlsConfig), +}); +export type UsageApiConfigSchema = TypeOf; export const configSchema = schema.object({ enabled: schema.boolean({ defaultValue: false }), diff --git a/x-pack/plugins/security_solution_serverless/server/constants.ts b/x-pack/plugins/security_solution_serverless/server/constants.ts index f4fcad6b760c6..411a7209682de 100644 --- a/x-pack/plugins/security_solution_serverless/server/constants.ts +++ b/x-pack/plugins/security_solution_serverless/server/constants.ts @@ -9,4 +9,5 @@ const namespace = 'elastic-system'; const USAGE_SERVICE_BASE_API_URL = `https://usage-api.${namespace}/api`; const USAGE_SERVICE_BASE_API_URL_V1 = `${USAGE_SERVICE_BASE_API_URL}/v1`; export const USAGE_SERVICE_USAGE_URL = `${USAGE_SERVICE_BASE_API_URL_V1}/usage`; +export const USAGE_REPORTING_ENDPOINT = '/api/v1/usage'; export const METERING_SERVICE_BATCH_SIZE = 1000; diff --git a/x-pack/plugins/security_solution_serverless/server/plugin.ts b/x-pack/plugins/security_solution_serverless/server/plugin.ts index 7161c5b684505..c249e48ca13a0 100644 --- a/x-pack/plugins/security_solution_serverless/server/plugin.ts +++ b/x-pack/plugins/security_solution_serverless/server/plugin.ts @@ -34,6 +34,7 @@ import { } from './endpoint/services'; import { NLPCleanupTask } from './task_manager/nlp_cleanup_task/nlp_cleanup_task'; import { telemetryEvents } from './telemetry/event_based_telemetry'; +import { UsageReportingService } from './common/services/usage_reporting_service'; export class SecuritySolutionServerlessPlugin implements @@ -49,11 +50,14 @@ export class SecuritySolutionServerlessPlugin private endpointUsageReportingTask: SecurityUsageReportingTask | undefined; private nlpCleanupTask: NLPCleanupTask | undefined; private readonly logger: Logger; + private readonly usageReportingService: UsageReportingService; constructor(private readonly initializerContext: PluginInitializerContext) { this.config = this.initializerContext.config.get(); this.logger = this.initializerContext.logger.get(); + this.usageReportingService = new UsageReportingService(this.config.usageApi); + const productTypesStr = JSON.stringify(this.config.productTypes, null, 2); this.logger.info(`Security Solution running with product types:\n${productTypesStr}`); } @@ -83,6 +87,7 @@ export class SecuritySolutionServerlessPlugin taskTitle: cloudSecurityMetringTaskProperties.taskTitle, version: cloudSecurityMetringTaskProperties.version, meteringCallback: cloudSecurityMetringTaskProperties.meteringCallback, + usageReportingService: this.usageReportingService, }); this.endpointUsageReportingTask = new SecurityUsageReportingTask({ @@ -95,6 +100,7 @@ export class SecuritySolutionServerlessPlugin meteringCallback: endpointMeteringService.getUsageRecords, taskManager: pluginsSetup.taskManager, cloudSetup: pluginsSetup.cloud, + usageReportingService: this.usageReportingService, }); this.nlpCleanupTask = new NLPCleanupTask({ diff --git a/x-pack/plugins/security_solution_serverless/server/task_manager/usage_reporting_task.test.ts b/x-pack/plugins/security_solution_serverless/server/task_manager/usage_reporting_task.test.ts index 66307e8f8a693..01c38ed6eed31 100644 --- a/x-pack/plugins/security_solution_serverless/server/task_manager/usage_reporting_task.test.ts +++ b/x-pack/plugins/security_solution_serverless/server/task_manager/usage_reporting_task.test.ts @@ -7,28 +7,26 @@ import { assign } from 'lodash'; +import type { SearchResponse } from '@elastic/elasticsearch/lib/api/types'; import type { CoreSetup, ElasticsearchClient } from '@kbn/core/server'; import type { TaskManagerSetupContract, ConcreteTaskInstance, } from '@kbn/task-manager-plugin/server'; import type { CloudSetup } from '@kbn/cloud-plugin/server'; + import { TaskStatus } from '@kbn/task-manager-plugin/server'; import { getDeleteTaskRunResult } from '@kbn/task-manager-plugin/server/task'; import { coreMock } from '@kbn/core/server/mocks'; import { loggingSystemMock } from '@kbn/core-logging-server-mocks'; import { taskManagerMock } from '@kbn/task-manager-plugin/server/mocks'; -import { ProductLine, ProductTier } from '../../common/product'; - -import { usageReportingService } from '../common/services'; import type { ServerlessSecurityConfig } from '../config'; import type { SecurityUsageReportingTaskSetupContract, UsageRecord } from '../types'; +import { ProductLine, ProductTier } from '../../common/product'; import { SecurityUsageReportingTask } from './usage_reporting_task'; import { endpointMeteringService } from '../endpoint/services'; -import type { SearchResponse } from '@elastic/elasticsearch/lib/api/types'; -import { USAGE_SERVICE_USAGE_URL } from '../constants'; describe('SecurityUsageReportingTask', () => { const TITLE = 'test-task-title'; @@ -45,7 +43,7 @@ describe('SecurityUsageReportingTask', () => { let mockEsClient: jest.Mocked; let mockCore: CoreSetup; let mockTaskManagerSetup: jest.Mocked; - let reportUsageSpy: jest.SpyInstance; + let reportUsageMock: jest.Mock; let meteringCallbackMock: jest.Mock; let taskArgs: SecurityUsageReportingTaskSetupContract; let usageRecord: UsageRecord; @@ -118,11 +116,24 @@ describe('SecurityUsageReportingTask', () => { taskTitle: TITLE, version: VERSION, meteringCallback: meteringCallbackMock, + usageReportingService: { + reportUsage: reportUsageMock, + }, }, overrides ); } + const USAGE_API_CONFIG = { + enabled: true, + url: 'https://usage-api-url', + tls: { + certificate: '', + key: '', + ca: '', + }, + }; + async function runTask(taskInstance = buildMockTaskInstance(), callNum: number = 0) { const mockTaskManagerStart = tmStartMock(); await mockTask.start({ taskManager: mockTaskManagerStart, interval: '5m' }); @@ -138,7 +149,7 @@ describe('SecurityUsageReportingTask', () => { .asInternalUser as jest.Mocked; mockTaskManagerSetup = tmSetupMock(); usageRecord = buildUsageRecord(); - reportUsageSpy = jest.spyOn(usageReportingService, 'reportUsage'); + reportUsageMock = jest.fn(); } describe('meteringCallback integration', () => { @@ -150,7 +161,7 @@ describe('SecurityUsageReportingTask', () => { productTypes: [ { product_line: ProductLine.endpoint, product_tier: ProductTier.complete }, ], - usageApi: { url: USAGE_SERVICE_USAGE_URL }, + usageApi: USAGE_API_CONFIG, } as ServerlessSecurityConfig, }); mockTask = new SecurityUsageReportingTask(taskArgs); @@ -199,9 +210,9 @@ describe('SecurityUsageReportingTask', () => { await runTasksUntilNoRunAt(); - expect(reportUsageSpy).toHaveBeenCalledTimes(3); + expect(reportUsageMock).toHaveBeenCalledTimes(3); batches.forEach((batch, i) => { - expect(reportUsageSpy).toHaveBeenNthCalledWith( + expect(reportUsageMock).toHaveBeenNthCalledWith( i + 1, expect.arrayContaining( batch.map(({ _source }) => @@ -209,8 +220,7 @@ describe('SecurityUsageReportingTask', () => { id: `endpoint-${_source.agent.id}-2021-09-01T00:00:00.000Z`, }) ) - ), - USAGE_SERVICE_USAGE_URL + ) ); }); }); @@ -227,7 +237,7 @@ describe('SecurityUsageReportingTask', () => { }); taskArgs = buildTaskArgs({ config: { - usageApi: { url: USAGE_SERVICE_USAGE_URL }, + usageApi: USAGE_API_CONFIG, } as ServerlessSecurityConfig, }); mockTask = new SecurityUsageReportingTask(taskArgs); @@ -273,7 +283,7 @@ describe('SecurityUsageReportingTask', () => { it('should report metering records', async () => { await runTask(); - expect(reportUsageSpy).toHaveBeenCalledWith( + expect(reportUsageMock).toHaveBeenCalledWith( expect.arrayContaining([ expect.objectContaining({ creation_timestamp: usageRecord.creation_timestamp, @@ -286,8 +296,7 @@ describe('SecurityUsageReportingTask', () => { usage: { period_seconds: 3600, quantity: 1, type: USAGE_TYPE }, usage_timestamp: usageRecord.usage_timestamp, }), - ]), - USAGE_SERVICE_USAGE_URL + ]) ); }); @@ -296,12 +305,12 @@ describe('SecurityUsageReportingTask', () => { expect(result).toEqual(getDeleteTaskRunResult()); - expect(reportUsageSpy).not.toHaveBeenCalled(); + expect(reportUsageMock).not.toHaveBeenCalled(); expect(meteringCallbackMock).not.toHaveBeenCalled(); }); describe('lastSuccessfulReport', () => { it('should set lastSuccessfulReport correctly if report success', async () => { - reportUsageSpy.mockResolvedValueOnce({ status: 201 }); + reportUsageMock.mockResolvedValueOnce({ status: 201 }); const taskInstance = buildMockTaskInstance(); const task = await runTask(taskInstance); const newLastSuccessfulReport = task?.state.lastSuccessfulReport; @@ -320,7 +329,7 @@ describe('SecurityUsageReportingTask', () => { describe('and response is NOT 201', () => { beforeEach(() => { - reportUsageSpy.mockResolvedValueOnce({ status: 500 }); + reportUsageMock.mockResolvedValueOnce({ status: 500 }); }); it('should set lastSuccessfulReport correctly', async () => { diff --git a/x-pack/plugins/security_solution_serverless/server/task_manager/usage_reporting_task.ts b/x-pack/plugins/security_solution_serverless/server/task_manager/usage_reporting_task.ts index 83ef25a849f2d..6eb682a84d474 100644 --- a/x-pack/plugins/security_solution_serverless/server/task_manager/usage_reporting_task.ts +++ b/x-pack/plugins/security_solution_serverless/server/task_manager/usage_reporting_task.ts @@ -8,10 +8,10 @@ import type { Response } from 'node-fetch'; import type { CoreSetup, Logger } from '@kbn/core/server'; import type { ConcreteTaskInstance } from '@kbn/task-manager-plugin/server'; -import { getDeleteTaskRunResult } from '@kbn/task-manager-plugin/server/task'; import type { CloudSetup } from '@kbn/cloud-plugin/server'; -import { usageReportingService } from '../common/services'; +import { getDeleteTaskRunResult } from '@kbn/task-manager-plugin/server/task'; + import type { MeteringCallback, SecurityUsageReportingTaskStartContract, @@ -19,6 +19,7 @@ import type { UsageRecord, } from '../types'; import type { ServerlessSecurityConfig } from '../config'; +import type { UsageReportingService } from '../common/services/usage_reporting_service'; import { stateSchemaByVersion, emptyState } from './task_state'; @@ -34,6 +35,7 @@ export class SecurityUsageReportingTask { private readonly version: string; private readonly logger: Logger; private readonly config: ServerlessSecurityConfig; + private readonly usageReportingService: UsageReportingService; constructor(setupContract: SecurityUsageReportingTaskSetupContract) { const { @@ -46,6 +48,7 @@ export class SecurityUsageReportingTask { taskTitle, version, meteringCallback, + usageReportingService, } = setupContract; this.cloudSetup = cloudSetup; @@ -53,6 +56,7 @@ export class SecurityUsageReportingTask { this.version = version; this.logger = logFactory.get(this.taskId); this.config = config; + this.usageReportingService = usageReportingService; try { taskManager.registerTaskDefinitions({ @@ -163,10 +167,7 @@ export class SecurityUsageReportingTask { try { this.logger.debug(`Sending ${usageRecords.length} usage records to the API`); - usageReportResponse = await usageReportingService.reportUsage( - usageRecords, - this.config.usageApi?.url - ); + usageReportResponse = await this.usageReportingService.reportUsage(usageRecords); if (!usageReportResponse.ok) { const errorResponse = await usageReportResponse.json(); diff --git a/x-pack/plugins/security_solution_serverless/server/types.ts b/x-pack/plugins/security_solution_serverless/server/types.ts index 4f3a7bf3c3db0..a838c410793c3 100644 --- a/x-pack/plugins/security_solution_serverless/server/types.ts +++ b/x-pack/plugins/security_solution_serverless/server/types.ts @@ -25,6 +25,7 @@ import type { IntegrationAssistantPluginSetup } from '@kbn/integration-assistant import type { ProductTier } from '../common/product'; import type { ServerlessSecurityConfig } from './config'; +import type { UsageReportingService } from './common/services/usage_reporting_service'; // eslint-disable-next-line @typescript-eslint/no-empty-interface export interface SecuritySolutionServerlessPluginSetup {} @@ -86,6 +87,7 @@ export interface SecurityUsageReportingTaskSetupContract { taskTitle: string; version: string; meteringCallback: MeteringCallback; + usageReportingService: UsageReportingService; } export interface SecurityUsageReportingTaskStartContract { diff --git a/x-pack/plugins/security_solution_serverless/tsconfig.json b/x-pack/plugins/security_solution_serverless/tsconfig.json index 55a4882655dc7..cb0518fc4dcd5 100644 --- a/x-pack/plugins/security_solution_serverless/tsconfig.json +++ b/x-pack/plugins/security_solution_serverless/tsconfig.json @@ -19,6 +19,7 @@ "@kbn/security-plugin", "@kbn/security-solution-ess", "@kbn/security-solution-plugin", + "@kbn/server-http-tools", "@kbn/serverless", "@kbn/security-solution-navigation", "@kbn/security-solution-upselling", @@ -46,5 +47,6 @@ "@kbn/logging", "@kbn/integration-assistant-plugin", "@kbn/cloud-security-posture-common", + "@kbn/dev-utils" ] } diff --git a/x-pack/plugins/stack_alerts/public/rule_types/es_query/expression/es_query_expression.test.tsx b/x-pack/plugins/stack_alerts/public/rule_types/es_query/expression/es_query_expression.test.tsx index 6686d56173de4..e1d17b79e612d 100644 --- a/x-pack/plugins/stack_alerts/public/rule_types/es_query/expression/es_query_expression.test.tsx +++ b/x-pack/plugins/stack_alerts/public/rule_types/es_query/expression/es_query_expression.test.tsx @@ -6,7 +6,6 @@ */ import React from 'react'; -import 'brace'; import { of, Subject } from 'rxjs'; import { mountWithIntl, nextTick } from '@kbn/test-jest-helpers'; import { act } from 'react-dom/test-utils'; diff --git a/x-pack/plugins/stack_alerts/public/rule_types/es_query/expression/expression.test.tsx b/x-pack/plugins/stack_alerts/public/rule_types/es_query/expression/expression.test.tsx index 64d075b7ba723..568a8cf226ae2 100644 --- a/x-pack/plugins/stack_alerts/public/rule_types/es_query/expression/expression.test.tsx +++ b/x-pack/plugins/stack_alerts/public/rule_types/es_query/expression/expression.test.tsx @@ -6,7 +6,6 @@ */ import type { DataView } from '@kbn/data-views-plugin/public'; import { mountWithIntl } from '@kbn/test-jest-helpers'; -import 'brace'; import React, { useState } from 'react'; import { docLinksServiceMock } from '@kbn/core/public/mocks'; import { httpServiceMock } from '@kbn/core/public/mocks'; diff --git a/x-pack/plugins/stack_alerts/public/rule_types/es_query/expression/expression.tsx b/x-pack/plugins/stack_alerts/public/rule_types/es_query/expression/expression.tsx index 196d138c68964..2f0c46a5e34c5 100644 --- a/x-pack/plugins/stack_alerts/public/rule_types/es_query/expression/expression.tsx +++ b/x-pack/plugins/stack_alerts/public/rule_types/es_query/expression/expression.tsx @@ -7,7 +7,6 @@ import React, { memo, PropsWithChildren, useCallback } from 'react'; import deepEqual from 'fast-deep-equal'; -import 'brace/theme/github'; import { EuiCallOut, EuiHorizontalRule, EuiSpacer } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import { RuleTypeParamsExpressionProps } from '@kbn/triggers-actions-ui-plugin/public'; diff --git a/x-pack/plugins/stack_connectors/common/openai/constants.ts b/x-pack/plugins/stack_connectors/common/openai/constants.ts index c57720d9847af..3d629360d03f3 100644 --- a/x-pack/plugins/stack_connectors/common/openai/constants.ts +++ b/x-pack/plugins/stack_connectors/common/openai/constants.ts @@ -27,6 +27,7 @@ export enum SUB_ACTION { export enum OpenAiProviderType { OpenAi = 'OpenAI', AzureAi = 'Azure OpenAI', + Other = 'Other', } export const DEFAULT_TIMEOUT_MS = 120000; diff --git a/x-pack/plugins/stack_connectors/common/openai/schema.ts b/x-pack/plugins/stack_connectors/common/openai/schema.ts index f62ee1f35174c..8a08da157b163 100644 --- a/x-pack/plugins/stack_connectors/common/openai/schema.ts +++ b/x-pack/plugins/stack_connectors/common/openai/schema.ts @@ -21,6 +21,12 @@ export const ConfigSchema = schema.oneOf([ defaultModel: schema.string({ defaultValue: DEFAULT_OPENAI_MODEL }), headers: schema.maybe(schema.recordOf(schema.string(), schema.string())), }), + schema.object({ + apiProvider: schema.oneOf([schema.literal(OpenAiProviderType.Other)]), + apiUrl: schema.string(), + defaultModel: schema.string(), + headers: schema.maybe(schema.recordOf(schema.string(), schema.string())), + }), ]); export const SecretsSchema = schema.object({ apiKey: schema.string() }); diff --git a/x-pack/plugins/stack_connectors/public/connector_types/lib/gen_ai/use_get_dashboard.test.ts b/x-pack/plugins/stack_connectors/public/connector_types/lib/gen_ai/use_get_dashboard.test.ts index 8ca9b97292fa3..18bcdc6232792 100644 --- a/x-pack/plugins/stack_connectors/public/connector_types/lib/gen_ai/use_get_dashboard.test.ts +++ b/x-pack/plugins/stack_connectors/public/connector_types/lib/gen_ai/use_get_dashboard.test.ts @@ -53,6 +53,7 @@ describe('useGetDashboard', () => { it.each([ ['Azure OpenAI', 'openai'], ['OpenAI', 'openai'], + ['Other', 'openai'], ['Bedrock', 'bedrock'], ])( 'fetches the %p dashboard and sets the dashboard URL with %p', diff --git a/x-pack/plugins/stack_connectors/public/connector_types/openai/connector.test.tsx b/x-pack/plugins/stack_connectors/public/connector_types/openai/connector.test.tsx index 03d41dd01caa9..2c8eaf8a76257 100644 --- a/x-pack/plugins/stack_connectors/public/connector_types/openai/connector.test.tsx +++ b/x-pack/plugins/stack_connectors/public/connector_types/openai/connector.test.tsx @@ -50,6 +50,17 @@ const azureConnector = { apiKey: 'thats-a-nice-looking-key', }, }; +const otherOpenAiConnector = { + ...openAiConnector, + config: { + apiUrl: 'https://localhost/oss-llm', + apiProvider: OpenAiProviderType.Other, + defaultModel: 'local-model', + }, + secrets: { + apiKey: 'thats-a-nice-looking-key', + }, +}; const navigateToUrl = jest.fn(); @@ -93,6 +104,24 @@ describe('ConnectorFields renders', () => { expect(getAllByTestId('azure-ai-api-keys-doc')[0]).toBeInTheDocument(); }); + test('other open ai connector fields are rendered', async () => { + const { getAllByTestId } = render( + + {}} /> + + ); + expect(getAllByTestId('config.apiUrl-input')[0]).toBeInTheDocument(); + expect(getAllByTestId('config.apiUrl-input')[0]).toHaveValue( + otherOpenAiConnector.config.apiUrl + ); + expect(getAllByTestId('config.apiProvider-select')[0]).toBeInTheDocument(); + expect(getAllByTestId('config.apiProvider-select')[0]).toHaveValue( + otherOpenAiConnector.config.apiProvider + ); + expect(getAllByTestId('other-ai-api-doc')[0]).toBeInTheDocument(); + expect(getAllByTestId('other-ai-api-keys-doc')[0]).toBeInTheDocument(); + }); + describe('Dashboard link', () => { it('Does not render if isEdit is false and dashboardUrl is defined', async () => { const { queryByTestId } = render( diff --git a/x-pack/plugins/stack_connectors/public/connector_types/openai/connector.tsx b/x-pack/plugins/stack_connectors/public/connector_types/openai/connector.tsx index c940ad76e3643..27cbb9a4dac08 100644 --- a/x-pack/plugins/stack_connectors/public/connector_types/openai/connector.tsx +++ b/x-pack/plugins/stack_connectors/public/connector_types/openai/connector.tsx @@ -24,6 +24,8 @@ import * as i18n from './translations'; import { azureAiConfig, azureAiSecrets, + otherOpenAiConfig, + otherOpenAiSecrets, openAiConfig, openAiSecrets, providerOptions, @@ -85,6 +87,14 @@ const ConnectorFields: React.FC = ({ readOnly, isEdi secretsFormSchema={azureAiSecrets} /> )} + {config != null && config.apiProvider === OpenAiProviderType.Other && ( + + )} {isEdit && ( + {`${i18n.OTHER_OPENAI} ${i18n.DOCUMENTATION}`} + + ), + }} + /> + ), + }, + { + id: 'defaultModel', + label: i18n.DEFAULT_MODEL_LABEL, + helpText: ( + + ), + }, +]; + export const openAiSecrets: SecretsFieldSchema[] = [ { id: 'apiKey', @@ -142,6 +177,31 @@ export const azureAiSecrets: SecretsFieldSchema[] = [ }, ]; +export const otherOpenAiSecrets: SecretsFieldSchema[] = [ + { + id: 'apiKey', + label: i18n.API_KEY_LABEL, + isPasswordField: true, + helpText: ( + + {`${i18n.OTHER_OPENAI} ${i18n.DOCUMENTATION}`} + + ), + }} + /> + ), + }, +]; + export const providerOptions = [ { value: OpenAiProviderType.OpenAi, @@ -153,4 +213,9 @@ export const providerOptions = [ text: i18n.AZURE_AI, label: i18n.AZURE_AI, }, + { + value: OpenAiProviderType.Other, + text: i18n.OTHER_OPENAI, + label: i18n.OTHER_OPENAI, + }, ]; diff --git a/x-pack/plugins/stack_connectors/public/connector_types/openai/params.test.tsx b/x-pack/plugins/stack_connectors/public/connector_types/openai/params.test.tsx index 09a2652ad8f1d..7539cc6bf6373 100644 --- a/x-pack/plugins/stack_connectors/public/connector_types/openai/params.test.tsx +++ b/x-pack/plugins/stack_connectors/public/connector_types/openai/params.test.tsx @@ -37,7 +37,7 @@ describe('Gen AI Params Fields renders', () => { expect(getByTestId('bodyJsonEditor')).toHaveProperty('value', '{"message": "test"}'); expect(getByTestId('bodyAddVariableButton')).toBeInTheDocument(); }); - test.each([OpenAiProviderType.OpenAi, OpenAiProviderType.AzureAi])( + test.each([OpenAiProviderType.OpenAi, OpenAiProviderType.AzureAi, OpenAiProviderType.Other])( 'useEffect handles the case when subAction and subActionParams are undefined and apiProvider is %p', (apiProvider) => { const actionParams = { @@ -79,6 +79,9 @@ describe('Gen AI Params Fields renders', () => { if (apiProvider === OpenAiProviderType.AzureAi) { expect(editAction).toHaveBeenCalledWith('subActionParams', { body: DEFAULT_BODY_AZURE }, 0); } + if (apiProvider === OpenAiProviderType.Other) { + expect(editAction).toHaveBeenCalledWith('subActionParams', { body: DEFAULT_BODY }, 0); + } } ); diff --git a/x-pack/plugins/stack_connectors/public/connector_types/openai/translations.ts b/x-pack/plugins/stack_connectors/public/connector_types/openai/translations.ts index 4c72866c6ece4..55815faac1c8e 100644 --- a/x-pack/plugins/stack_connectors/public/connector_types/openai/translations.ts +++ b/x-pack/plugins/stack_connectors/public/connector_types/openai/translations.ts @@ -47,6 +47,10 @@ export const AZURE_AI = i18n.translate('xpack.stackConnectors.components.genAi.a defaultMessage: 'Azure OpenAI', }); +export const OTHER_OPENAI = i18n.translate('xpack.stackConnectors.components.genAi.otherAi', { + defaultMessage: 'Other (OpenAI Compatible Service)', +}); + export const DOCUMENTATION = i18n.translate( 'xpack.stackConnectors.components.genAi.documentation', { diff --git a/x-pack/plugins/stack_connectors/server/connector_types/openai/index.ts b/x-pack/plugins/stack_connectors/server/connector_types/openai/index.ts index f8a3a3d32ddb2..5bf0ba6c3a562 100644 --- a/x-pack/plugins/stack_connectors/server/connector_types/openai/index.ts +++ b/x-pack/plugins/stack_connectors/server/connector_types/openai/index.ts @@ -53,7 +53,11 @@ export const configValidator = (configObject: Config, validatorServices: Validat const { apiProvider } = configObject; - if (apiProvider !== OpenAiProviderType.OpenAi && apiProvider !== OpenAiProviderType.AzureAi) { + if ( + apiProvider !== OpenAiProviderType.OpenAi && + apiProvider !== OpenAiProviderType.AzureAi && + apiProvider !== OpenAiProviderType.Other + ) { throw new Error( `API Provider is not supported${ apiProvider && (apiProvider as OpenAiProviderType).length ? `: ${apiProvider}` : `` diff --git a/x-pack/plugins/stack_connectors/server/connector_types/openai/lib/other_openai_utils.test.ts b/x-pack/plugins/stack_connectors/server/connector_types/openai/lib/other_openai_utils.test.ts new file mode 100644 index 0000000000000..33722314f5422 --- /dev/null +++ b/x-pack/plugins/stack_connectors/server/connector_types/openai/lib/other_openai_utils.test.ts @@ -0,0 +1,116 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { sanitizeRequest, getRequestWithStreamOption } from './other_openai_utils'; + +describe('Other (OpenAI Compatible Service) Utils', () => { + describe('sanitizeRequest', () => { + it('sets stream to false when stream is set to true in the body', () => { + const body = { + model: 'mistral', + stream: true, + messages: [ + { + role: 'user', + content: 'This is a test', + }, + ], + }; + + const sanitizedBodyString = sanitizeRequest(JSON.stringify(body)); + expect(sanitizedBodyString).toEqual( + `{\"model\":\"mistral\",\"stream\":false,\"messages\":[{\"role\":\"user\",\"content\":\"This is a test\"}]}` + ); + }); + + it('sets stream to false when stream is not defined in the body', () => { + const body = { + model: 'mistral', + messages: [ + { + role: 'user', + content: 'This is a test', + }, + ], + }; + + const sanitizedBodyString = sanitizeRequest(JSON.stringify(body)); + expect(sanitizedBodyString).toEqual( + `{\"model\":\"mistral\",\"messages\":[{\"role\":\"user\",\"content\":\"This is a test\"}],\"stream\":false}` + ); + }); + + it('sets stream to false when stream is set to false in the body', () => { + const body = { + model: 'mistral', + stream: false, + messages: [ + { + role: 'user', + content: 'This is a test', + }, + ], + }; + + const sanitizedBodyString = sanitizeRequest(JSON.stringify(body)); + expect(sanitizedBodyString).toEqual( + `{\"model\":\"mistral\",\"stream\":false,\"messages\":[{\"role\":\"user\",\"content\":\"This is a test\"}]}` + ); + }); + + it('does nothing when body is malformed JSON', () => { + const bodyString = `{\"model\":\"mistral\",\"messages\":[{\"role\":\"user\",\"content\":\"This is a test\"}],,}`; + + const sanitizedBodyString = sanitizeRequest(bodyString); + expect(sanitizedBodyString).toEqual(bodyString); + }); + }); + + describe('getRequestWithStreamOption', () => { + it('sets stream parameter when stream is not defined in the body', () => { + const body = { + model: 'mistral', + messages: [ + { + role: 'user', + content: 'This is a test', + }, + ], + }; + + const sanitizedBodyString = getRequestWithStreamOption(JSON.stringify(body), true); + expect(sanitizedBodyString).toEqual( + `{\"model\":\"mistral\",\"messages\":[{\"role\":\"user\",\"content\":\"This is a test\"}],\"stream\":true}` + ); + }); + + it('overrides stream parameter if defined in body', () => { + const body = { + model: 'mistral', + stream: true, + messages: [ + { + role: 'user', + content: 'This is a test', + }, + ], + }; + + const sanitizedBodyString = getRequestWithStreamOption(JSON.stringify(body), false); + expect(sanitizedBodyString).toEqual( + `{\"model\":\"mistral\",\"stream\":false,\"messages\":[{\"role\":\"user\",\"content\":\"This is a test\"}]}` + ); + }); + + it('does nothing when body is malformed JSON', () => { + const bodyString = `{\"model\":\"mistral\",\"messages\":[{\"role\":\"user\",\"content\":\"This is a test\"}],,}`; + + const sanitizedBodyString = getRequestWithStreamOption(bodyString, false); + expect(sanitizedBodyString).toEqual(bodyString); + }); + }); +}); diff --git a/x-pack/plugins/stack_connectors/server/connector_types/openai/lib/other_openai_utils.ts b/x-pack/plugins/stack_connectors/server/connector_types/openai/lib/other_openai_utils.ts new file mode 100644 index 0000000000000..8288e0dba9ad1 --- /dev/null +++ b/x-pack/plugins/stack_connectors/server/connector_types/openai/lib/other_openai_utils.ts @@ -0,0 +1,39 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +/** + * Sanitizes the Other (OpenAI Compatible Service) request body to set stream to false + * so users cannot specify a streaming response when the framework + * is not prepared to handle streaming + * + * The stream parameter is accepted in the ChatCompletion + * API and the Completion API only + */ +export const sanitizeRequest = (body: string): string => { + return getRequestWithStreamOption(body, false); +}; + +/** + * Intercepts the Other (OpenAI Compatible Service) request body to set the stream parameter + * + * The stream parameter is accepted in the ChatCompletion + * API and the Completion API only + */ +export const getRequestWithStreamOption = (body: string, stream: boolean): string => { + try { + const jsonBody = JSON.parse(body); + if (jsonBody) { + jsonBody.stream = stream; + } + + return JSON.stringify(jsonBody); + } catch (err) { + // swallow the error + } + + return body; +}; diff --git a/x-pack/plugins/stack_connectors/server/connector_types/openai/lib/utils.test.ts b/x-pack/plugins/stack_connectors/server/connector_types/openai/lib/utils.test.ts index 9dffaab3e5e00..142f3a319eeb6 100644 --- a/x-pack/plugins/stack_connectors/server/connector_types/openai/lib/utils.test.ts +++ b/x-pack/plugins/stack_connectors/server/connector_types/openai/lib/utils.test.ts @@ -19,8 +19,14 @@ import { sanitizeRequest as azureAiSanitizeRequest, getRequestWithStreamOption as azureAiGetRequestWithStreamOption, } from './azure_openai_utils'; +import { + sanitizeRequest as otherOpenAiSanitizeRequest, + getRequestWithStreamOption as otherOpenAiGetRequestWithStreamOption, +} from './other_openai_utils'; + jest.mock('./openai_utils'); jest.mock('./azure_openai_utils'); +jest.mock('./other_openai_utils'); describe('Utils', () => { const azureAiUrl = @@ -38,6 +44,7 @@ describe('Utils', () => { describe('sanitizeRequest', () => { const mockOpenAiSanitizeRequest = openAiSanitizeRequest as jest.Mock; const mockAzureAiSanitizeRequest = azureAiSanitizeRequest as jest.Mock; + const mockOtherOpenAiSanitizeRequest = otherOpenAiSanitizeRequest as jest.Mock; beforeEach(() => { jest.clearAllMocks(); }); @@ -50,24 +57,36 @@ describe('Utils', () => { DEFAULT_OPENAI_MODEL ); expect(mockAzureAiSanitizeRequest).not.toHaveBeenCalled(); + expect(mockOtherOpenAiSanitizeRequest).not.toHaveBeenCalled(); + }); + + it('calls other_openai_utils sanitizeRequest when provider is Other OpenAi', () => { + sanitizeRequest(OpenAiProviderType.Other, OPENAI_CHAT_URL, bodyString, DEFAULT_OPENAI_MODEL); + expect(mockOtherOpenAiSanitizeRequest).toHaveBeenCalledWith(bodyString); + expect(mockOpenAiSanitizeRequest).not.toHaveBeenCalled(); + expect(mockAzureAiSanitizeRequest).not.toHaveBeenCalled(); }); it('calls azure_openai_utils sanitizeRequest when provider is AzureAi', () => { sanitizeRequest(OpenAiProviderType.AzureAi, azureAiUrl, bodyString); expect(mockAzureAiSanitizeRequest).toHaveBeenCalledWith(azureAiUrl, bodyString); expect(mockOpenAiSanitizeRequest).not.toHaveBeenCalled(); + expect(mockOtherOpenAiSanitizeRequest).not.toHaveBeenCalled(); }); it('does not call any helper fns when provider is unrecognized', () => { sanitizeRequest('foo', OPENAI_CHAT_URL, bodyString); expect(mockOpenAiSanitizeRequest).not.toHaveBeenCalled(); expect(mockAzureAiSanitizeRequest).not.toHaveBeenCalled(); + expect(mockOtherOpenAiSanitizeRequest).not.toHaveBeenCalled(); }); }); describe('getRequestWithStreamOption', () => { const mockOpenAiGetRequestWithStreamOption = openAiGetRequestWithStreamOption as jest.Mock; const mockAzureAiGetRequestWithStreamOption = azureAiGetRequestWithStreamOption as jest.Mock; + const mockOtherOpenAiGetRequestWithStreamOption = + otherOpenAiGetRequestWithStreamOption as jest.Mock; beforeEach(() => { jest.clearAllMocks(); }); @@ -88,6 +107,15 @@ describe('Utils', () => { DEFAULT_OPENAI_MODEL ); expect(mockAzureAiGetRequestWithStreamOption).not.toHaveBeenCalled(); + expect(mockOtherOpenAiGetRequestWithStreamOption).not.toHaveBeenCalled(); + }); + + it('calls other_openai_utils getRequestWithStreamOption when provider is Other OpenAi', () => { + getRequestWithStreamOption(OpenAiProviderType.Other, OPENAI_CHAT_URL, bodyString, true); + + expect(mockOtherOpenAiGetRequestWithStreamOption).toHaveBeenCalledWith(bodyString, true); + expect(mockOpenAiGetRequestWithStreamOption).not.toHaveBeenCalled(); + expect(mockAzureAiGetRequestWithStreamOption).not.toHaveBeenCalled(); }); it('calls azure_openai_utils getRequestWithStreamOption when provider is AzureAi', () => { @@ -99,6 +127,7 @@ describe('Utils', () => { true ); expect(mockOpenAiGetRequestWithStreamOption).not.toHaveBeenCalled(); + expect(mockOtherOpenAiGetRequestWithStreamOption).not.toHaveBeenCalled(); }); it('does not call any helper fns when provider is unrecognized', () => { @@ -110,6 +139,7 @@ describe('Utils', () => { ); expect(mockOpenAiGetRequestWithStreamOption).not.toHaveBeenCalled(); expect(mockAzureAiGetRequestWithStreamOption).not.toHaveBeenCalled(); + expect(mockOtherOpenAiGetRequestWithStreamOption).not.toHaveBeenCalled(); }); }); @@ -127,6 +157,19 @@ describe('Utils', () => { }); }); + it('returns correct axios options when provider is other openai and stream is false', () => { + expect(getAxiosOptions(OpenAiProviderType.Other, 'api-abc', false)).toEqual({ + headers: { Authorization: `Bearer api-abc`, ['content-type']: 'application/json' }, + }); + }); + + it('returns correct axios options when provider is other openai and stream is true', () => { + expect(getAxiosOptions(OpenAiProviderType.Other, 'api-abc', true)).toEqual({ + headers: { Authorization: `Bearer api-abc`, ['content-type']: 'application/json' }, + responseType: 'stream', + }); + }); + it('returns correct axios options when provider is azure openai and stream is false', () => { expect(getAxiosOptions(OpenAiProviderType.AzureAi, 'api-abc', false)).toEqual({ headers: { ['api-key']: `api-abc`, ['content-type']: 'application/json' }, diff --git a/x-pack/plugins/stack_connectors/server/connector_types/openai/lib/utils.ts b/x-pack/plugins/stack_connectors/server/connector_types/openai/lib/utils.ts index 811dfd4ce63b4..3028433656503 100644 --- a/x-pack/plugins/stack_connectors/server/connector_types/openai/lib/utils.ts +++ b/x-pack/plugins/stack_connectors/server/connector_types/openai/lib/utils.ts @@ -16,6 +16,10 @@ import { sanitizeRequest as azureAiSanitizeRequest, getRequestWithStreamOption as azureAiGetRequestWithStreamOption, } from './azure_openai_utils'; +import { + sanitizeRequest as otherOpenAiSanitizeRequest, + getRequestWithStreamOption as otherOpenAiGetRequestWithStreamOption, +} from './other_openai_utils'; export const sanitizeRequest = ( provider: string, @@ -28,6 +32,8 @@ export const sanitizeRequest = ( return openAiSanitizeRequest(url, body, defaultModel!); case OpenAiProviderType.AzureAi: return azureAiSanitizeRequest(url, body); + case OpenAiProviderType.Other: + return otherOpenAiSanitizeRequest(body); default: return body; } @@ -42,7 +48,7 @@ export function getRequestWithStreamOption( ): string; export function getRequestWithStreamOption( - provider: OpenAiProviderType.AzureAi, + provider: OpenAiProviderType.AzureAi | OpenAiProviderType.Other, url: string, body: string, stream: boolean @@ -68,6 +74,8 @@ export function getRequestWithStreamOption( return openAiGetRequestWithStreamOption(url, body, stream, defaultModel!); case OpenAiProviderType.AzureAi: return azureAiGetRequestWithStreamOption(url, body, stream); + case OpenAiProviderType.Other: + return otherOpenAiGetRequestWithStreamOption(body, stream); default: return body; } @@ -81,6 +89,7 @@ export const getAxiosOptions = ( const responseType = stream ? { responseType: 'stream' as ResponseType } : {}; switch (provider) { case OpenAiProviderType.OpenAi: + case OpenAiProviderType.Other: return { headers: { Authorization: `Bearer ${apiKey}`, ['content-type']: 'application/json' }, ...responseType, diff --git a/x-pack/plugins/stack_connectors/server/connector_types/openai/openai.test.ts b/x-pack/plugins/stack_connectors/server/connector_types/openai/openai.test.ts index 87dacaf4e6f17..1362b7610e2cd 100644 --- a/x-pack/plugins/stack_connectors/server/connector_types/openai/openai.test.ts +++ b/x-pack/plugins/stack_connectors/server/connector_types/openai/openai.test.ts @@ -20,6 +20,9 @@ import { RunActionResponseSchema, StreamingResponseSchema } from '../../../commo import { initDashboard } from '../lib/gen_ai/create_gen_ai_dashboard'; import { PassThrough, Transform } from 'stream'; import { ConnectorUsageCollector } from '@kbn/actions-plugin/server/types'; + +const DEFAULT_OTHER_OPENAI_MODEL = 'local-model'; + jest.mock('../lib/gen_ai/create_gen_ai_dashboard'); const mockTee = jest.fn(); @@ -713,6 +716,431 @@ describe('OpenAIConnector', () => { }); }); + describe('Other OpenAI', () => { + const connector = new OpenAIConnector({ + configurationUtilities: actionsConfigMock.create(), + connector: { id: '1', type: OPENAI_CONNECTOR_ID }, + config: { + apiUrl: 'http://localhost:1234/v1/chat/completions', + apiProvider: OpenAiProviderType.Other, + defaultModel: DEFAULT_OTHER_OPENAI_MODEL, + headers: { + 'X-My-Custom-Header': 'foo', + Authorization: 'override', + }, + }, + secrets: { apiKey: '123' }, + logger, + services: actionsMock.createServices(), + }); + + const sampleOpenAiBody = { + model: DEFAULT_OTHER_OPENAI_MODEL, + messages: [ + { + role: 'user', + content: 'Hello world', + }, + ], + }; + + beforeEach(() => { + // @ts-ignore + connector.request = mockRequest; + jest.clearAllMocks(); + }); + + describe('runApi', () => { + it('the Other OpenAI API call is successful with correct parameters', async () => { + const response = await connector.runApi( + { body: JSON.stringify(sampleOpenAiBody) }, + connectorUsageCollector + ); + expect(mockRequest).toBeCalledTimes(1); + expect(mockRequest).toHaveBeenCalledWith( + { + ...mockDefaults, + url: 'http://localhost:1234/v1/chat/completions', + data: JSON.stringify({ + ...sampleOpenAiBody, + stream: false, + model: DEFAULT_OTHER_OPENAI_MODEL, + }), + headers: { + Authorization: 'Bearer 123', + 'X-My-Custom-Header': 'foo', + 'content-type': 'application/json', + }, + }, + connectorUsageCollector + ); + expect(response).toEqual(mockResponse.data); + }); + + it('overrides stream parameter if set in the body', async () => { + const body = { + model: 'llama-3.1', + messages: [ + { + role: 'user', + content: 'Hello world', + }, + ], + }; + const response = await connector.runApi( + { + body: JSON.stringify({ + ...body, + stream: true, + }), + }, + connectorUsageCollector + ); + expect(mockRequest).toBeCalledTimes(1); + expect(mockRequest).toHaveBeenCalledWith( + { + ...mockDefaults, + url: 'http://localhost:1234/v1/chat/completions', + data: JSON.stringify({ + ...body, + stream: false, + }), + headers: { + Authorization: 'Bearer 123', + 'X-My-Custom-Header': 'foo', + 'content-type': 'application/json', + }, + }, + connectorUsageCollector + ); + expect(response).toEqual(mockResponse.data); + }); + + it('errors during API calls are properly handled', async () => { + // @ts-ignore + connector.request = mockError; + + await expect( + connector.runApi({ body: JSON.stringify(sampleOpenAiBody) }, connectorUsageCollector) + ).rejects.toThrow('API Error'); + }); + }); + + describe('streamApi', () => { + it('the Other OpenAI API call is successful with correct parameters when stream = false', async () => { + const response = await connector.streamApi( + { + body: JSON.stringify(sampleOpenAiBody), + stream: false, + }, + connectorUsageCollector + ); + expect(mockRequest).toBeCalledTimes(1); + expect(mockRequest).toHaveBeenCalledWith( + { + url: 'http://localhost:1234/v1/chat/completions', + method: 'post', + responseSchema: RunActionResponseSchema, + data: JSON.stringify({ + ...sampleOpenAiBody, + stream: false, + }), + headers: { + Authorization: 'Bearer 123', + 'X-My-Custom-Header': 'foo', + 'content-type': 'application/json', + }, + }, + connectorUsageCollector + ); + expect(response).toEqual(mockResponse.data); + }); + + it('the Other OpenAI API call is successful with correct parameters when stream = true', async () => { + const response = await connector.streamApi( + { + body: JSON.stringify(sampleOpenAiBody), + stream: true, + }, + connectorUsageCollector + ); + expect(mockRequest).toBeCalledTimes(1); + expect(mockRequest).toHaveBeenCalledWith( + { + responseType: 'stream', + url: 'http://localhost:1234/v1/chat/completions', + method: 'post', + responseSchema: StreamingResponseSchema, + data: JSON.stringify({ + ...sampleOpenAiBody, + stream: true, + model: DEFAULT_OTHER_OPENAI_MODEL, + }), + headers: { + Authorization: 'Bearer 123', + 'X-My-Custom-Header': 'foo', + 'content-type': 'application/json', + }, + }, + connectorUsageCollector + ); + expect(response).toEqual({ + headers: { 'Content-Type': 'dont-compress-this' }, + ...mockResponse.data, + }); + }); + + it('overrides stream parameter if set in the body with explicit stream parameter', async () => { + const body = { + model: 'llama-3.1', + messages: [ + { + role: 'user', + content: 'Hello world', + }, + ], + }; + const response = await connector.streamApi( + { + body: JSON.stringify({ + ...body, + stream: false, + }), + stream: true, + }, + connectorUsageCollector + ); + expect(mockRequest).toBeCalledTimes(1); + expect(mockRequest).toHaveBeenCalledWith( + { + responseType: 'stream', + url: 'http://localhost:1234/v1/chat/completions', + method: 'post', + responseSchema: StreamingResponseSchema, + data: JSON.stringify({ + ...body, + stream: true, + }), + headers: { + Authorization: 'Bearer 123', + 'X-My-Custom-Header': 'foo', + 'content-type': 'application/json', + }, + }, + connectorUsageCollector + ); + expect(response).toEqual({ + headers: { 'Content-Type': 'dont-compress-this' }, + ...mockResponse.data, + }); + }); + + it('errors during API calls are properly handled', async () => { + // @ts-ignore + connector.request = mockError; + + await expect( + connector.streamApi( + { body: JSON.stringify(sampleOpenAiBody), stream: true }, + connectorUsageCollector + ) + ).rejects.toThrow('API Error'); + }); + }); + + describe('invokeStream', () => { + const mockStream = ( + dataToStream: string[] = [ + 'data: {"object":"chat.completion.chunk","choices":[{"delta":{"content":"My"}}]}\ndata: {"object":"chat.completion.chunk","choices":[{"delta":{"content":" new"}}]}', + ] + ) => { + const streamMock = createStreamMock(); + dataToStream.forEach((chunk) => { + streamMock.write(chunk); + }); + streamMock.complete(); + mockRequest = jest.fn().mockResolvedValue({ ...mockResponse, data: streamMock.transform }); + return mockRequest; + }; + beforeEach(() => { + // @ts-ignore + connector.request = mockStream(); + }); + + it('the API call is successful with correct request parameters', async () => { + await connector.invokeStream(sampleOpenAiBody, connectorUsageCollector); + expect(mockRequest).toBeCalledTimes(1); + expect(mockRequest).toHaveBeenCalledWith( + { + url: 'http://localhost:1234/v1/chat/completions', + method: 'post', + responseSchema: StreamingResponseSchema, + responseType: 'stream', + data: JSON.stringify({ + ...sampleOpenAiBody, + stream: true, + }), + headers: { + Authorization: 'Bearer 123', + 'X-My-Custom-Header': 'foo', + 'content-type': 'application/json', + }, + }, + connectorUsageCollector + ); + }); + + it('signal is properly passed to streamApi', async () => { + const signal = jest.fn(); + await connector.invokeStream({ ...sampleOpenAiBody, signal }, connectorUsageCollector); + + expect(mockRequest).toHaveBeenCalledWith( + { + url: 'http://localhost:1234/v1/chat/completions', + method: 'post', + responseSchema: StreamingResponseSchema, + responseType: 'stream', + data: JSON.stringify({ + ...sampleOpenAiBody, + stream: true, + }), + headers: { + Authorization: 'Bearer 123', + 'X-My-Custom-Header': 'foo', + 'content-type': 'application/json', + }, + signal, + }, + connectorUsageCollector + ); + }); + + it('timeout is properly passed to streamApi', async () => { + const timeout = 180000; + await connector.invokeStream({ ...sampleOpenAiBody, timeout }, connectorUsageCollector); + + expect(mockRequest).toHaveBeenCalledWith( + { + url: 'http://localhost:1234/v1/chat/completions', + method: 'post', + responseSchema: StreamingResponseSchema, + responseType: 'stream', + data: JSON.stringify({ + ...sampleOpenAiBody, + stream: true, + }), + headers: { + Authorization: 'Bearer 123', + 'X-My-Custom-Header': 'foo', + 'content-type': 'application/json', + }, + timeout, + }, + connectorUsageCollector + ); + }); + + it('errors during API calls are properly handled', async () => { + // @ts-ignore + connector.request = mockError; + + await expect( + connector.invokeStream(sampleOpenAiBody, connectorUsageCollector) + ).rejects.toThrow('API Error'); + }); + + it('responds with a readable stream', async () => { + // @ts-ignore + connector.request = mockStream(); + const response = await connector.invokeStream(sampleOpenAiBody, connectorUsageCollector); + expect(response instanceof PassThrough).toEqual(true); + }); + }); + + describe('invokeAI', () => { + it('the API call is successful with correct parameters', async () => { + const response = await connector.invokeAI(sampleOpenAiBody, connectorUsageCollector); + expect(mockRequest).toBeCalledTimes(1); + expect(mockRequest).toHaveBeenCalledWith( + { + ...mockDefaults, + url: 'http://localhost:1234/v1/chat/completions', + data: JSON.stringify({ + ...sampleOpenAiBody, + stream: false, + model: DEFAULT_OTHER_OPENAI_MODEL, + }), + headers: { + Authorization: 'Bearer 123', + 'X-My-Custom-Header': 'foo', + 'content-type': 'application/json', + }, + }, + connectorUsageCollector + ); + expect(response.message).toEqual(mockResponseString); + expect(response.usage.total_tokens).toEqual(9); + }); + + it('signal is properly passed to runApi', async () => { + const signal = jest.fn(); + await connector.invokeAI({ ...sampleOpenAiBody, signal }, connectorUsageCollector); + + expect(mockRequest).toHaveBeenCalledWith( + { + ...mockDefaults, + url: 'http://localhost:1234/v1/chat/completions', + data: JSON.stringify({ + ...sampleOpenAiBody, + stream: false, + model: DEFAULT_OTHER_OPENAI_MODEL, + }), + headers: { + Authorization: 'Bearer 123', + 'X-My-Custom-Header': 'foo', + 'content-type': 'application/json', + }, + signal, + }, + connectorUsageCollector + ); + }); + + it('timeout is properly passed to runApi', async () => { + const timeout = 180000; + await connector.invokeAI({ ...sampleOpenAiBody, timeout }, connectorUsageCollector); + + expect(mockRequest).toHaveBeenCalledWith( + { + ...mockDefaults, + url: 'http://localhost:1234/v1/chat/completions', + data: JSON.stringify({ + ...sampleOpenAiBody, + stream: false, + model: DEFAULT_OTHER_OPENAI_MODEL, + }), + headers: { + Authorization: 'Bearer 123', + 'X-My-Custom-Header': 'foo', + 'content-type': 'application/json', + }, + timeout, + }, + connectorUsageCollector + ); + }); + + it('errors during API calls are properly handled', async () => { + // @ts-ignore + connector.request = mockError; + + await expect(connector.invokeAI(sampleOpenAiBody, connectorUsageCollector)).rejects.toThrow( + 'API Error' + ); + }); + }); + }); + describe('AzureAI', () => { const connector = new OpenAIConnector({ configurationUtilities: actionsConfigMock.create(), diff --git a/x-pack/plugins/task_manager/server/integration_tests/removed_types.test.ts b/x-pack/plugins/task_manager/server/integration_tests/removed_types.test.ts index 69bf717b95fc6..aeb182c4794e6 100644 --- a/x-pack/plugins/task_manager/server/integration_tests/removed_types.test.ts +++ b/x-pack/plugins/task_manager/server/integration_tests/removed_types.test.ts @@ -121,7 +121,15 @@ describe('unrecognized task types', () => { // so we want to wait that long to let it refresh await new Promise((r) => setTimeout(r, 5100)); - expect(errorLogSpy).not.toHaveBeenCalled(); + const errorLogCalls = errorLogSpy.mock.calls[0]; + + // if there are any error logs, none of them should be workload aggregator errors + if (errorLogCalls) { + // should be no workload aggregator errors + for (const elog of errorLogCalls) { + expect(elog).not.toMatch(/^\[WorkloadAggregator\]: Error: Unsupported task type/i); + } + } }); }); diff --git a/x-pack/plugins/telemetry_collection_xpack/schema/xpack_plugins.json b/x-pack/plugins/telemetry_collection_xpack/schema/xpack_plugins.json index 0de2cbd77db7b..0e5d4156d9760 100644 --- a/x-pack/plugins/telemetry_collection_xpack/schema/xpack_plugins.json +++ b/x-pack/plugins/telemetry_collection_xpack/schema/xpack_plugins.json @@ -73,6 +73,9 @@ }, "[OpenAI]": { "type": "long" + }, + "[Other]": { + "type": "long" } } }, diff --git a/x-pack/plugins/transform/public/app/sections/transform_management/components/search_selection/search_selection.tsx b/x-pack/plugins/transform/public/app/sections/transform_management/components/search_selection/search_selection.tsx index 3c6fcc67f0c7e..7c0b03f7b9856 100644 --- a/x-pack/plugins/transform/public/app/sections/transform_management/components/search_selection/search_selection.tsx +++ b/x-pack/plugins/transform/public/app/sections/transform_management/components/search_selection/search_selection.tsx @@ -11,6 +11,7 @@ import { FormattedMessage } from '@kbn/i18n-react'; import React, { type FC, Fragment } from 'react'; import { SavedObjectFinder } from '@kbn/saved-objects-finder-plugin/public'; +import type { FinderAttributes, SavedObjectCommon } from '@kbn/saved-objects-finder-plugin/common'; import { useAppDependencies } from '../../../../app_dependencies'; interface SearchSelectionProps { @@ -19,6 +20,8 @@ interface SearchSelectionProps { canEditDataView: boolean; } +type SavedObject = SavedObjectCommon; + const fixedPageSize: number = 8; export const SearchSelection: FC = ({ @@ -64,6 +67,9 @@ export const SearchSelection: FC = ({ defaultMessage: 'Saved search', } ), + showSavedObject: (savedObject: SavedObject) => + // ES|QL Based saved searches are not supported in transforms, filter them out + savedObject.attributes.isTextBasedQuery !== true, }, { type: 'index-pattern', diff --git a/x-pack/plugins/translations/translations/fr-FR.json b/x-pack/plugins/translations/translations/fr-FR.json index d65dddae39479..41d1b6ab8b3d1 100644 --- a/x-pack/plugins/translations/translations/fr-FR.json +++ b/x-pack/plugins/translations/translations/fr-FR.json @@ -320,8 +320,6 @@ "coloring.dynamicColoring.rangeType.label": "Type de valeur", "coloring.dynamicColoring.rangeType.number": "Numéro", "coloring.dynamicColoring.rangeType.percent": "Pourcent", - "console.autocomplete.addMethodMetaText": "méthode", - "console.autocomplete.fieldsFetchingAnnotation": "La récupération des champs est en cours", "console.autocompleteSuggestions.apiLabel": "API", "console.autocompleteSuggestions.endpointLabel": "point de terminaison", "console.autocompleteSuggestions.methodLabel": "méthode", @@ -362,10 +360,6 @@ "console.loadingError.title": "Impossible de charger la console", "console.notification.clearHistory": "Effacer l'historique", "console.notification.disableSavingToHistory": "Désactiver l'enregistrement", - "console.notification.error.couldNotSaveRequestTitle": "Impossible d'enregistrer la requête dans l'historique de la console.", - "console.notification.error.historyQuotaReachedMessage": "L'historique des requêtes est arrivé à saturation. Effacez l'historique de la console ou désactivez l'enregistrement de nouvelles requêtes.", - "console.notification.error.noRequestSelectedTitle": "Aucune requête sélectionnée. Sélectionnez une requête en positionnant le curseur dessus.", - "console.notification.error.unknownErrorTitle": "Erreur de requête inconnue", "console.pageHeading": "Console", "console.requestInProgressBadgeText": "Requête en cours", "console.requestOptions.autoIndentButtonLabel": "Appliquer les indentations", @@ -7274,9 +7268,6 @@ "sharedUXPackages.fileUpload.uploadCompleteButtonLabel": "Chargement terminé", "sharedUXPackages.fileUpload.uploadDoneToolTipContent": "Votre fichier a bien été chargé !", "sharedUXPackages.fileUpload.uploadingButtonLabel": "Chargement", - "sharedUXPackages.no_data_views.esqlButtonLabel": "Langue : ES|QL", - "sharedUXPackages.no_data_views.esqlDocsLink": "En savoir plus.", - "sharedUXPackages.no_data_views.esqlMessage": "Vous pouvez aussi rechercher vos données en utilisant directement ES|QL. {docsLink}", "sharedUXPackages.noDataConfig.addIntegrationsDescription": "Utilisez Elastic Agent pour collecter des données et créer des solutions Analytics.", "sharedUXPackages.noDataConfig.addIntegrationsTitle": "Ajouter des intégrations", "sharedUXPackages.noDataConfig.analytics": "Analyse", @@ -7298,8 +7289,6 @@ "sharedUXPackages.noDataViewsPrompt.dataViewExplanation": "Les vues de données identifient les données Elasticsearch que vous souhaitez explorer. Vous pouvez faire pointer des vues de données vers un ou plusieurs flux de données, index et alias d'index, tels que vos données de log d'hier, ou vers tous les index contenant vos données de log.", "sharedUXPackages.noDataViewsPrompt.learnMore": "Envie d'en savoir plus ?", "sharedUXPackages.noDataViewsPrompt.noPermission.dataViewExplanation": "Les vues de données identifient les données Elasticsearch que vous souhaitez explorer. Pour créer des vues de données, demandez les autorisations requises à votre administrateur.", - "sharedUXPackages.noDataViewsPrompt.noPermission.title": "Vous devez disposer d'une autorisation pour pouvoir créer des vues de données", - "sharedUXPackages.noDataViewsPrompt.nowCreate": "Créez à présent une vue de données.", "sharedUXPackages.noDataViewsPrompt.readDocumentation": "Lisez les documents", "sharedUXPackages.noDataViewsPrompt.youHaveData": "Vous avez des données dans Elasticsearch.", "sharedUXPackages.prompt.errors.notFound.body": "Désolé, la page que vous recherchez est introuvable. Elle a peut-être été retirée ou renommée, ou peut-être qu'elle n'a jamais existé.", @@ -9387,6 +9376,94 @@ "xpack.actions.serverSideErrors.unavailableLicenseInformationErrorMessage": "Les actions sont indisponibles - les informations de licence ne sont pas disponibles actuellement.", "xpack.actions.subActionsFramework.urlValidationError": "Erreur lors de la validation de l'URL : {message}", "xpack.actions.urlAllowedHostsConfigurationError": "Le {field} cible \"{value}\" n'est pas ajouté à la configuration Kibana xpack.actions.allowedHosts", + "xpack.aiAssistant.askAssistantButton.buttonLabel": "Demander à l'assistant", + "xpack.aiAssistant.askAssistantButton.popoverContent": "Obtenez des informations relatives à vos données grâce à l'assistant d'Elastic", + "xpack.aiAssistant.assistantSetup.title": "Bienvenue sur l'assistant d'intelligence artificielle d'Elastic", + "xpack.aiAssistant.chatActionsMenu.euiButtonIcon.menuLabel": "Menu", + "xpack.aiAssistant.chatActionsMenu.euiToolTip.moreActionsLabel": "Plus d'actions", + "xpack.aiAssistant.chatCollapsedItems.hideEvents": "Masquer {count} événements", + "xpack.aiAssistant.chatCollapsedItems.showEvents": "Montrer {count} événements", + "xpack.aiAssistant.chatCollapsedItems.toggleButtonLabel": "Afficher/masquer les éléments", + "xpack.aiAssistant.chatFlyout.euiButtonIcon.expandConversationListLabel": "Développer la liste des conversations", + "xpack.aiAssistant.chatFlyout.euiButtonIcon.newChatLabel": "Nouveau chat", + "xpack.aiAssistant.chatFlyout.euiToolTip.collapseConversationListLabel": "Réduire la liste des conversations", + "xpack.aiAssistant.chatFlyout.euiToolTip.expandConversationListLabel": "Développer la liste des conversations", + "xpack.aiAssistant.chatFlyout.euiToolTip.newChatLabel": "Nouveau chat", + "xpack.aiAssistant.chatHeader.actions.connector": "Connecteur", + "xpack.aiAssistant.chatHeader.actions.copyConversation": "Copier la conversation", + "xpack.aiAssistant.chatHeader.actions.knowledgeBase": "Gérer la base de connaissances", + "xpack.aiAssistant.chatHeader.actions.settings": "Réglages de l'assistant d'IA", + "xpack.aiAssistant.chatHeader.actions.title": "Actions", + "xpack.aiAssistant.chatHeader.editConversationInput": "Modifier la conversation", + "xpack.aiAssistant.chatHeader.euiButtonIcon.navigateToConversationsLabel": "Accéder aux conversations", + "xpack.aiAssistant.chatHeader.euiButtonIcon.toggleFlyoutModeLabel": "Afficher / Masquer le mode menu volant", + "xpack.aiAssistant.chatHeader.euiToolTip.flyoutModeLabel.dock": "Ancrer le chat", + "xpack.aiAssistant.chatHeader.euiToolTip.flyoutModeLabel.undock": "Désancrer le chat", + "xpack.aiAssistant.chatHeader.euiToolTip.navigateToConversationsLabel": "Accéder aux conversations", + "xpack.aiAssistant.chatPromptEditor.codeEditor.payloadEditorLabel": "payloadEditor", + "xpack.aiAssistant.chatPromptEditor.euiButtonIcon.submitLabel": "Envoyer", + "xpack.aiAssistant.chatTimeline.actions.copyMessage": "Copier le message", + "xpack.aiAssistant.chatTimeline.actions.copyMessageSuccessful": "Message copié", + "xpack.aiAssistant.chatTimeline.actions.editPrompt": "Modifier l'invite", + "xpack.aiAssistant.chatTimeline.actions.inspectPrompt": "Inspecter l'invite", + "xpack.aiAssistant.chatTimeline.messages.elasticAssistant.label": "Assistant d'Elastic", + "xpack.aiAssistant.chatTimeline.messages.system.label": "Système", + "xpack.aiAssistant.chatTimeline.messages.user.label": "Vous", + "xpack.aiAssistant.checkingKbAvailability": "Vérification de la disponibilité de la base de connaissances", + "xpack.aiAssistant.conversationStartTitle": "a démarré une conversation", + "xpack.aiAssistant.couldNotFindConversationContent": "Impossible de trouver une conversation avec l'ID {conversationId}. Assurez-vous que la conversation existe et que vous y avez accès.", + "xpack.aiAssistant.couldNotFindConversationTitle": "Conversation introuvable", + "xpack.aiAssistant.disclaimer.disclaimerLabel": "Ce chat est soutenu par une intégration avec votre fournisseur LLM. Il arrive que les grands modèles de langage (LLM) présentent comme correctes des informations incorrectes. Elastic prend en charge la configuration ainsi que la connexion au fournisseur LLM et à votre base de connaissances, mais n'est pas responsable des réponses fournies par le LLM.", + "xpack.aiAssistant.emptyConversationTitle": "Nouvelle conversation", + "xpack.aiAssistant.errorSettingUpKnowledgeBase": "Impossible de configurer la base de connaissances", + "xpack.aiAssistant.errorUpdatingConversation": "Impossible de mettre à jour la conversation", + "xpack.aiAssistant.executedFunctionFailureEvent": "impossible d'exécuter la fonction {functionName}", + "xpack.aiAssistant.failedToGetStatus": "Échec de l'obtention du statut du modèle.", + "xpack.aiAssistant.failedToSetupKnowledgeBase": "Échec de la configuration de la base de connaissances.", + "xpack.aiAssistant.flyout.confirmDeleteButtonText": "Supprimer la conversation", + "xpack.aiAssistant.flyout.confirmDeleteConversationContent": "Cette action ne peut pas être annulée.", + "xpack.aiAssistant.flyout.confirmDeleteConversationTitle": "Supprimer cette conversation ?", + "xpack.aiAssistant.flyout.failedToDeleteConversation": "Impossible de supprimer la conversation", + "xpack.aiAssistant.functionListPopover.euiButtonIcon.selectAFunctionLabel": "Sélectionner la fonction", + "xpack.aiAssistant.functionListPopover.euiToolTip.clearFunction": "Effacer la fonction", + "xpack.aiAssistant.functionListPopover.euiToolTip.selectAFunctionLabel": "Sélectionner une fonction", + "xpack.aiAssistant.hideExpandConversationButton.hide": "Masquer les chats", + "xpack.aiAssistant.hideExpandConversationButton.show": "Afficher les chats", + "xpack.aiAssistant.incorrectLicense.body": "Une licence d'entreprise est requise pour utiliser l'assistant d'intelligence artificielle d'Elastic.", + "xpack.aiAssistant.incorrectLicense.manageLicense": "Gérer la licence", + "xpack.aiAssistant.incorrectLicense.subscriptionPlansButton": "Plans d'abonnement", + "xpack.aiAssistant.incorrectLicense.title": "Mettez votre licence à niveau", + "xpack.aiAssistant.initialSetupPanel.setupConnector.buttonLabel": "Configurer un connecteur GenAI", + "xpack.aiAssistant.initialSetupPanel.setupConnector.description2": "Commencez à travailler avec l'assistant AI Elastic en configurant un connecteur pour votre fournisseur d'IA. Le modèle doit prendre en charge les appels de fonction. Lorsque vous utilisez OpenAI ou Azure, nous vous recommandons d'utiliser GPT4.", + "xpack.aiAssistant.installingKb": "Configuration de la base de connaissances", + "xpack.aiAssistant.newChatButton": "Nouveau chat", + "xpack.aiAssistant.poweredByModel": "Alimenté par {model}", + "xpack.aiAssistant.prompt.functionList.filter": "Filtre", + "xpack.aiAssistant.prompt.functionList.functionList": "Liste de fonctions", + "xpack.aiAssistant.prompt.placeholder": "Envoyer un message à l'assistant", + "xpack.aiAssistant.promptEditorNaturalLanguage.euiSelectable.selectAnOptionLabel": "Sélectionner une option", + "xpack.aiAssistant.settingsPage.goToConnectorsButtonLabel": "Gérer les connecteurs", + "xpack.aiAssistant.setupKb": "Améliorez votre expérience en configurant la base de connaissances.", + "xpack.aiAssistant.simulatedFunctionCallingCalloutLabel": "L'appel de fonctions simulées est activé. Vous risquez de voir les performances se dégrader.", + "xpack.aiAssistant.suggestedFunctionEvent": "a demandé la fonction {functionName}", + "xpack.aiAssistant.technicalPreviewBadgeDescription": "GTP4 est nécessaire pour bénéficier d'une meilleure expérience avec les appels de fonctions (par exemple lors de la réalisation d'analyse de la cause d'un problème, de la visualisation de données et autres). GPT3.5 peut fonctionner pour certains des workflows les plus simples comme les explications d'erreurs ou pour bénéficier d'une expérience comparable à ChatGPT au sein de Kibana à partir du moment où les appels de fonctions ne sont pas fréquents.", + "xpack.aiAssistant.userExecutedFunctionEvent": "a exécuté la fonction {functionName}", + "xpack.aiAssistant.userSuggestedFunctionEvent": "a demandé la fonction {functionName}", + "xpack.aiAssistant.welcomeMessage.div.checkTrainedModelsToLabel": " {retryInstallingLink} ou vérifiez {trainedModelsLink} pour vous assurer que {modelName} est déployé et en cours d'exécution.", + "xpack.aiAssistant.welcomeMessage.div.settingUpKnowledgeBaseLabel": "Configuration de la base de connaissances", + "xpack.aiAssistant.welcomeMessage.inspectErrorsButtonEmptyLabel": "Inspecter les problèmes", + "xpack.aiAssistant.welcomeMessage.issuesDescriptionListTitleLabel": "Problèmes", + "xpack.aiAssistant.welcomeMessage.knowledgeBaseSuccessfullyInstalledLabel": "La base de connaissances a été installée avec succès", + "xpack.aiAssistant.welcomeMessage.modelIsNotDeployedLabel": "Le modèle {modelName} n'est pas déployé", + "xpack.aiAssistant.welcomeMessage.modelIsNotFullyAllocatedLabel": "L'état d'allocation de {modelName} est {allocationState}", + "xpack.aiAssistant.welcomeMessage.modelIsNotStartedLabel": "L'état de déploiement de {modelName} est {deploymentState}", + "xpack.aiAssistant.welcomeMessage.retryButtonLabel": "Installer la base de connaissances", + "xpack.aiAssistant.welcomeMessage.trainedModelsLinkLabel": "Modèles entraînés", + "xpack.aiAssistant.welcomeMessage.weAreSettingUpTextLabel": "Nous configurons votre base de connaissances. Cette opération peut prendre quelques minutes. Vous pouvez continuer à utiliser l'Assistant lors de ce processus.", + "xpack.aiAssistant.welcomeMessageConnectors.connectorsErrorTextLabel": "Impossible de charger les connecteurs", + "xpack.aiAssistant.welcomeMessageConnectors.connectorsForbiddenTextLabel": "Vous n'avez pas les autorisations requises pour charger les connecteurs", + "xpack.aiAssistant.welcomeMessageKnowledgeBase.yourKnowledgeBaseIsNotSetUpCorrectlyLabel": "Votre base de connaissances n'a pas été configurée.", + "xpack.aiAssistant.welcomeMessageKnowledgeBaseSetupErrorPanel.retryInstallingLinkLabel": "Réessayer l'installation", "xpack.aiops.actions.openChangePointInMlAppName": "Ouvrir dans AIOps Labs", "xpack.aiops.analysis.columnSelectorAriaLabel": "Filtrer les colonnes", "xpack.aiops.analysis.columnSelectorNotEnoughColumnsSelected": "Au moins une colonne doit être sélectionnée.", @@ -23213,7 +23290,6 @@ "xpack.infra.logs.highlights.highlightTermsFieldLabel": "Termes à mettre en surbrillance", "xpack.infra.logs.index.anomaliesTabTitle": "Anomalies", "xpack.infra.logs.index.logCategoriesBetaBadgeTitle": "Catégories", - "xpack.infra.logs.index.logsLabel": "Logs", "xpack.infra.logs.index.settingsTabTitle": "Paramètres", "xpack.infra.logs.index.streamTabTitle": "Flux", "xpack.infra.logs.logCategoriesTitle": "Catégories", @@ -28642,7 +28718,6 @@ "xpack.ml.jobsList.jobDetails.forecastsTable.processingTimeLabel": "Temps de traitement", "xpack.ml.jobsList.jobDetails.forecastsTable.statusLabel": "Statut", "xpack.ml.jobsList.jobDetails.forecastsTable.toLabel": "À", - "xpack.ml.jobsList.jobDetails.forecastsTable.viewAriaLabel": "Afficher la prévision créée le {createdDate}", "xpack.ml.jobsList.jobDetails.forecastsTable.viewLabel": "Afficher", "xpack.ml.jobsList.jobDetails.generalTitle": "Général", "xpack.ml.jobsList.jobDetails.influencersTitle": "Influenceurs", @@ -30071,7 +30146,6 @@ "xpack.ml.trainedModels.modelsList.builtInModelLabel": "intégré", "xpack.ml.trainedModels.modelsList.builtInModelMessage": "Modèle intégré", "xpack.ml.trainedModels.modelsList.collapseRow": "Réduire", - "xpack.ml.trainedModels.modelsList.createdAtHeader": "Créé à", "xpack.ml.trainedModels.modelsList.deleteDisabledWithDeploymentsTooltip": "Le modèle a commencé à être déployé", "xpack.ml.trainedModels.modelsList.deleteDisabledWithInferenceServicesTooltip": "Le modèle est utilisé par l'API _inference", "xpack.ml.trainedModels.modelsList.deleteModal.approvePipelinesDeletionLabel": "Supprimer {pipelinesCount, plural, one {le pipeline} other {les pipelines}}", @@ -30086,9 +30160,7 @@ "xpack.ml.trainedModels.modelsList.deleteModelsButtonLabel": "Supprimer", "xpack.ml.trainedModels.modelsList.deployModelActionLabel": "Déployer le modèle", "xpack.ml.trainedModels.modelsList.disableSelectableMessage": "Le modèle a des pipelines associés", - "xpack.ml.trainedModels.modelsList.downloadCompleteSuccess": "\"{modelIds}\" {modelIdsLength, plural, one {a été téléchargé} other {ont été téléchargés}} avec succès.", "xpack.ml.trainedModels.modelsList.downloadFailed": "Échec du téléchargement de \"{modelId}\"", - "xpack.ml.trainedModels.modelsList.downloadStatusCheckErrorMessage": "Échec de la vérification du statut du téléchargement", "xpack.ml.trainedModels.modelsList.e5Title": "E5 (EmbEddings from bidirEctional Encoder rEpresentations)", "xpack.ml.trainedModels.modelsList.e5v1Description": "E5 (EmbEddings from bidirEctional Encoder rEpresentations)", "xpack.ml.trainedModels.modelsList.e5v1x86Description": "E5 (EmbEddings from bidirEctional Encoder rEpresentations), optimisé for linux-x86_64", @@ -30126,11 +30198,9 @@ "xpack.ml.trainedModels.modelsList.forceStopDialog.selectDeploymentsLegend": "Sélectionner les déploiements à arrêter", "xpack.ml.trainedModels.modelsList.forceStopDialog.title": "Arrêter {deploymentCount, plural, one {le déploiement} other {les déploiements}} du modèle {modelId} ?", "xpack.ml.trainedModels.modelsList.mitLicenseLabel": "Licence : MIT", - "xpack.ml.trainedModels.modelsList.modelDescriptionHeader": "Description", "xpack.ml.trainedModels.modelsList.modelIdHeader": "ID", "xpack.ml.trainedModels.modelsList.modelState.downloadedName": "Paré au déploiement", "xpack.ml.trainedModels.modelsList.modelState.downloadingName": "Téléchargement...", - "xpack.ml.trainedModels.modelsList.modelState.notDownloadedName": "Non téléchargé", "xpack.ml.trainedModels.modelsList.modelState.startedName": "Déployé", "xpack.ml.trainedModels.modelsList.modelState.startingName": "Déploiement lancé...", "xpack.ml.trainedModels.modelsList.modelState.stoppingName": "Déploiement en phase d'arrêt...", @@ -30157,14 +30227,10 @@ "xpack.ml.trainedModels.modelsList.startDeployment.updateButton": "Mettre à jour", "xpack.ml.trainedModels.modelsList.startDeployment.viewElserDocLink": "Afficher la documentation", "xpack.ml.trainedModels.modelsList.startFailed": "Impossible de démarrer \"{modelId}\"", - "xpack.ml.trainedModels.modelsList.startSuccess": "Le déploiement pour \"{modelId}\" a bien été démarré.", "xpack.ml.trainedModels.modelsList.stateHeader": "État", "xpack.ml.trainedModels.modelsList.stopDeploymentWarning": "Impossible d'arrêter \"{deploymentId}\"", "xpack.ml.trainedModels.modelsList.stopFailed": "Impossible d'arrêter \"{modelId}\"", - "xpack.ml.trainedModels.modelsList.stopSuccess": "{numberOfDeployments, plural, one {Le déploiement} other {Les déploiements}} pour \"{modelId}\" {numberOfDeployments, plural, one {a bien été arrêté} other {ont bien été arrêtés}}.", - "xpack.ml.trainedModels.modelsList.successfullyDeletedMessage": "{modelsCount, plural, one {Le modèle {modelIds}} other {# modèles}} {modelsCount, plural, one {a bien été supprimé} other {ont bien été supprimés}}.", "xpack.ml.trainedModels.modelsList.totalAmountLabel": "Total de modèles entraînés", - "xpack.ml.trainedModels.modelsList.typeHeader": "Type", "xpack.ml.trainedModels.modelsList.updateDeployment.modalTitle": "Mettre à jour le déploiement {modelId}", "xpack.ml.trainedModels.modelsList.updateFailed": "Impossible de mettre à jour \"{modelId}\"", "xpack.ml.trainedModels.modelsList.updateSuccess": "Le déploiement pour \"{modelId}\" a bien été mis à jour.", @@ -32564,92 +32630,27 @@ "xpack.observabilityAiAssistant.app.starterPrompts.whatAreSlos.prompt": "Que signifie \"SLO\" ?", "xpack.observabilityAiAssistant.app.starterPrompts.whatAreSlos.title": "SLO", "xpack.observabilityAiAssistant.appTitle": "Assistant d'intelligence artificielle d'Observability", - "xpack.observabilityAiAssistant.askAssistantButton.buttonLabel": "Demander à l'assistant", - "xpack.observabilityAiAssistant.askAssistantButton.popoverContent": "Obtenez des informations relatives à vos données grâce à l'assistant d'Elastic", - "xpack.observabilityAiAssistant.askAssistantButton.popoverTitle": "Assistant d'IA pour Observability", - "xpack.observabilityAiAssistant.assistantSetup.title": "Bienvenue sur l'assistant d'intelligence artificielle d'Elastic", "xpack.observabilityAiAssistant.changesList.dotImpactHigh": "Élevé", "xpack.observabilityAiAssistant.changesList.dotImpactLow": "Bas", "xpack.observabilityAiAssistant.changesList.dotImpactMedium": "Moyenne", "xpack.observabilityAiAssistant.changesList.labelColumnTitle": "Étiquette", "xpack.observabilityAiAssistant.changesList.noChangesDetected": "Aucun changement détecté", "xpack.observabilityAiAssistant.changesList.trendColumnTitle": "Tendance", - "xpack.observabilityAiAssistant.chatActionsMenu.euiButtonIcon.menuLabel": "Menu", - "xpack.observabilityAiAssistant.chatActionsMenu.euiToolTip.moreActionsLabel": "Plus d'actions", - "xpack.observabilityAiAssistant.chatCollapsedItems.hideEvents": "Masquer {count} événements", - "xpack.observabilityAiAssistant.chatCollapsedItems.showEvents": "Montrer {count} événements", - "xpack.observabilityAiAssistant.chatCollapsedItems.toggleButtonLabel": "Afficher/masquer les éléments", "xpack.observabilityAiAssistant.chatCompletionError.conversationNotFoundError": "Conversation introuvable", "xpack.observabilityAiAssistant.chatCompletionError.internalServerError": "Une erreur s'est produite au niveau du serveur interne", "xpack.observabilityAiAssistant.chatCompletionError.tokenLimitReachedError": "Limite de token atteinte. La limite de token est {tokenLimit}, mais la conversation actuelle a {tokenCount} tokens.", - "xpack.observabilityAiAssistant.chatFlyout.euiButtonIcon.expandConversationListLabel": "Développer la liste des conversations", - "xpack.observabilityAiAssistant.chatFlyout.euiButtonIcon.newChatLabel": "Nouveau chat", - "xpack.observabilityAiAssistant.chatFlyout.euiFlyoutResizable.aiAssistantForObservabilityLabel": "Menu volant du chat de l'assistant d'IA pour Observability", - "xpack.observabilityAiAssistant.chatFlyout.euiToolTip.collapseConversationListLabel": "Réduire la liste des conversations", - "xpack.observabilityAiAssistant.chatFlyout.euiToolTip.expandConversationListLabel": "Développer la liste des conversations", - "xpack.observabilityAiAssistant.chatFlyout.euiToolTip.newChatLabel": "Nouveau chat", - "xpack.observabilityAiAssistant.chatHeader.actions.connector": "Connecteur", - "xpack.observabilityAiAssistant.chatHeader.actions.copyConversation": "Copier la conversation", - "xpack.observabilityAiAssistant.chatHeader.actions.knowledgeBase": "Gérer la base de connaissances", - "xpack.observabilityAiAssistant.chatHeader.actions.settings": "Réglages de l'assistant d'IA", - "xpack.observabilityAiAssistant.chatHeader.actions.title": "Actions", - "xpack.observabilityAiAssistant.chatHeader.editConversationInput": "Modifier la conversation", - "xpack.observabilityAiAssistant.chatHeader.euiButtonIcon.navigateToConversationsLabel": "Accéder aux conversations", - "xpack.observabilityAiAssistant.chatHeader.euiButtonIcon.toggleFlyoutModeLabel": "Afficher / Masquer le mode menu volant", - "xpack.observabilityAiAssistant.chatHeader.euiToolTip.flyoutModeLabel.dock": "Ancrer le chat", - "xpack.observabilityAiAssistant.chatHeader.euiToolTip.flyoutModeLabel.undock": "Désancrer le chat", - "xpack.observabilityAiAssistant.chatHeader.euiToolTip.navigateToConversationsLabel": "Accéder aux conversations", - "xpack.observabilityAiAssistant.chatPromptEditor.codeEditor.payloadEditorLabel": "payloadEditor", - "xpack.observabilityAiAssistant.chatPromptEditor.euiButtonIcon.submitLabel": "Envoyer", "xpack.observabilityAiAssistant.chatService.div.helloLabel": "Bonjour", - "xpack.observabilityAiAssistant.chatTimeline.actions.copyMessage": "Copier le message", - "xpack.observabilityAiAssistant.chatTimeline.actions.copyMessageSuccessful": "Message copié", - "xpack.observabilityAiAssistant.chatTimeline.actions.editPrompt": "Modifier l'invite", - "xpack.observabilityAiAssistant.chatTimeline.actions.inspectPrompt": "Inspecter l'invite", - "xpack.observabilityAiAssistant.chatTimeline.messages.elasticAssistant.label": "Assistant d'Elastic", - "xpack.observabilityAiAssistant.chatTimeline.messages.system.label": "Système", - "xpack.observabilityAiAssistant.chatTimeline.messages.user.label": "Vous", - "xpack.observabilityAiAssistant.checkingKbAvailability": "Vérification de la disponibilité de la base de connaissances", "xpack.observabilityAiAssistant.connectorSelector.connectorSelectLabel": "Connecteur :", "xpack.observabilityAiAssistant.connectorSelector.empty": "Aucun connecteur", "xpack.observabilityAiAssistant.connectorSelector.error": "Impossible de charger les connecteurs", - "xpack.observabilityAiAssistant.conversationList.deleteConversationIconLabel": "Supprimer", - "xpack.observabilityAiAssistant.conversationList.errorMessage": "Échec de chargement", - "xpack.observabilityAiAssistant.conversationList.noConversations": "Aucune conversation", - "xpack.observabilityAiAssistant.conversationList.title": "Précédemment", "xpack.observabilityAiAssistant.conversationsDeepLinkTitle": "Conversations", - "xpack.observabilityAiAssistant.conversationStartTitle": "a démarré une conversation", - "xpack.observabilityAiAssistant.couldNotFindConversationContent": "Impossible de trouver une conversation avec l'ID {conversationId}. Assurez-vous que la conversation existe et que vous y avez accès.", - "xpack.observabilityAiAssistant.couldNotFindConversationTitle": "Conversation introuvable", - "xpack.observabilityAiAssistant.disclaimer.disclaimerLabel": "Ce chat est soutenu par une intégration avec votre fournisseur LLM. Il arrive que les grands modèles de langage (LLM) présentent comme correctes des informations incorrectes. Elastic prend en charge la configuration ainsi que la connexion au fournisseur LLM et à votre base de connaissances, mais n'est pas responsable des réponses fournies par le LLM.", - "xpack.observabilityAiAssistant.emptyConversationTitle": "Nouvelle conversation", - "xpack.observabilityAiAssistant.errorSettingUpKnowledgeBase": "Impossible de configurer la base de connaissances", - "xpack.observabilityAiAssistant.errorUpdatingConversation": "Impossible de mettre à jour la conversation", - "xpack.observabilityAiAssistant.executedFunctionFailureEvent": "impossible d'exécuter la fonction {functionName}", "xpack.observabilityAiAssistant.experimentalTitle": "Version d'évaluation technique", "xpack.observabilityAiAssistant.experimentalTooltip": "Cette fonctionnalité est en version d'évaluation technique et pourra être modifiée ou retirée complètement dans une future version. Elastic s'efforcera de corriger tout problème, mais les fonctionnalités des versions d'évaluation technique ne sont pas soumises aux SLA de support des fonctionnalités officielles en disponibilité générale.", "xpack.observabilityAiAssistant.failedLoadingResponseText": "Échec de chargement de la réponse", - "xpack.observabilityAiAssistant.failedToGetStatus": "Échec de l'obtention du statut du modèle.", "xpack.observabilityAiAssistant.failedToLoadResponse": "Échec du chargement d'une réponse de l'assistant d'intelligence artificielle", - "xpack.observabilityAiAssistant.failedToSetupKnowledgeBase": "Échec de la configuration de la base de connaissances.", "xpack.observabilityAiAssistant.featureRegistry.featureName": "Assistant d'intelligence artificielle d'Observability", "xpack.observabilityAiAssistant.feedbackButtons.em.thanksForYourFeedbackLabel": "Merci pour vos retours", - "xpack.observabilityAiAssistant.flyout.confirmDeleteButtonText": "Supprimer la conversation", - "xpack.observabilityAiAssistant.flyout.confirmDeleteConversationContent": "Cette action ne peut pas être annulée.", - "xpack.observabilityAiAssistant.flyout.confirmDeleteConversationTitle": "Supprimer cette conversation ?", - "xpack.observabilityAiAssistant.flyout.failedToDeleteConversation": "Impossible de supprimer la conversation", "xpack.observabilityAiAssistant.functionCallLimitExceeded": "\n\nRemarque : l'Assistant a essayé d'appeler une fonction, même si la limite a été dépassée", - "xpack.observabilityAiAssistant.functionListPopover.euiButtonIcon.selectAFunctionLabel": "Sélectionner la fonction", - "xpack.observabilityAiAssistant.functionListPopover.euiToolTip.clearFunction": "Effacer la fonction", - "xpack.observabilityAiAssistant.functionListPopover.euiToolTip.selectAFunctionLabel": "Sélectionner une fonction", - "xpack.observabilityAiAssistant.hideExpandConversationButton.hide": "Masquer les chats", - "xpack.observabilityAiAssistant.hideExpandConversationButton.show": "Afficher les chats", - "xpack.observabilityAiAssistant.incorrectLicense.body": "Une licence d'entreprise est requise pour utiliser l'assistant d'intelligence artificielle d'Elastic.", - "xpack.observabilityAiAssistant.incorrectLicense.manageLicense": "Gérer la licence", - "xpack.observabilityAiAssistant.incorrectLicense.subscriptionPlansButton": "Plans d'abonnement", - "xpack.observabilityAiAssistant.incorrectLicense.title": "Mettez votre licence à niveau", - "xpack.observabilityAiAssistant.initialSetupPanel.setupConnector.buttonLabel": "Configurer un connecteur GenAI", - "xpack.observabilityAiAssistant.initialSetupPanel.setupConnector.description2": "Commencez à travailler avec l'assistant AI Elastic en configurant un connecteur pour votre fournisseur d'IA. Le modèle doit prendre en charge les appels de fonction. Lorsque vous utilisez OpenAI ou Azure, nous vous recommandons d'utiliser GPT4.", "xpack.observabilityAiAssistant.insight.actions": "Actions", "xpack.observabilityAiAssistant.insight.actions.connector": "Connecteur", "xpack.observabilityAiAssistant.insight.actions.editPrompt": "Modifier l'invite", @@ -32667,7 +32668,6 @@ "xpack.observabilityAiAssistant.insight.response.startChat": "Lancer le chat", "xpack.observabilityAiAssistant.insight.sendPromptEdit": "Envoyer l'invite", "xpack.observabilityAiAssistant.insightModifiedPrompt": "Cette information a été modifiée.", - "xpack.observabilityAiAssistant.installingKb": "Configuration de la base de connaissances", "xpack.observabilityAiAssistant.lensESQLFunction.displayChart": "Afficher le graphique", "xpack.observabilityAiAssistant.lensESQLFunction.displayTable": "Afficher le tableau", "xpack.observabilityAiAssistant.lensESQLFunction.edit": "Modifier la visualisation", @@ -32682,42 +32682,14 @@ "xpack.observabilityAiAssistant.missingCredentialsCallout.title": "Informations d'identification manquantes", "xpack.observabilityAiAssistant.navControl.initFailureErrorTitle": "Échec de l'initialisation de l'assistant d'IA d'Observability", "xpack.observabilityAiAssistant.navControl.openTheAIAssistantPopoverLabel": "Ouvrir l'assistant d'IA", - "xpack.observabilityAiAssistant.newChatButton": "Nouveau chat", - "xpack.observabilityAiAssistant.poweredByModel": "Alimenté par {model}", - "xpack.observabilityAiAssistant.prompt.functionList.filter": "Filtre", - "xpack.observabilityAiAssistant.prompt.functionList.functionList": "Liste de fonctions", - "xpack.observabilityAiAssistant.prompt.placeholder": "Envoyer un message à l'assistant", - "xpack.observabilityAiAssistant.promptEditorNaturalLanguage.euiSelectable.selectAnOptionLabel": "Sélectionner une option", "xpack.observabilityAiAssistant.regenerateResponseButtonLabel": "Régénérer", "xpack.observabilityAiAssistant.requiredConnectorField": "Connecteur obligatoire.", "xpack.observabilityAiAssistant.requiredMessageTextField": "Le message est requis.", "xpack.observabilityAiAssistant.resetDefaultPrompt": "Réinitialiser à la valeur par défaut", "xpack.observabilityAiAssistant.runThisQuery": "Afficher les résultats", - "xpack.observabilityAiAssistant.settingsPage.goToConnectorsButtonLabel": "Gérer les connecteurs", - "xpack.observabilityAiAssistant.setupKb": "Améliorez votre expérience en configurant la base de connaissances.", - "xpack.observabilityAiAssistant.simulatedFunctionCallingCalloutLabel": "L'appel de fonctions simulées est activé. Vous risquez de voir les performances se dégrader.", "xpack.observabilityAiAssistant.stopGeneratingButtonLabel": "Arrêter la génération", - "xpack.observabilityAiAssistant.suggestedFunctionEvent": "a demandé la fonction {functionName}", - "xpack.observabilityAiAssistant.technicalPreviewBadgeDescription": "GTP4 est nécessaire pour bénéficier d'une meilleure expérience avec les appels de fonctions (par exemple lors de la réalisation d'analyse de la cause d'un problème, de la visualisation de données et autres). GPT3.5 peut fonctionner pour certains des workflows les plus simples comme les explications d'erreurs ou pour bénéficier d'une expérience comparable à ChatGPT au sein de Kibana à partir du moment où les appels de fonctions ne sont pas fréquents.", "xpack.observabilityAiAssistant.tokenLimitError": "La conversation dépasse la limite de token. La limite de token maximale est **{tokenLimit}**, mais la conversation a **{tokenCount}** tokens. Veuillez démarrer une nouvelle conversation pour continuer.", - "xpack.observabilityAiAssistant.userExecutedFunctionEvent": "a exécuté la fonction {functionName}", - "xpack.observabilityAiAssistant.userSuggestedFunctionEvent": "a demandé la fonction {functionName}", "xpack.observabilityAiAssistant.visualizeThisQuery": "Visualiser cette requête", - "xpack.observabilityAiAssistant.welcomeMessage.div.checkTrainedModelsToLabel": " {retryInstallingLink} ou vérifiez {trainedModelsLink} pour vous assurer que {modelName} est déployé et en cours d'exécution.", - "xpack.observabilityAiAssistant.welcomeMessage.div.settingUpKnowledgeBaseLabel": "Configuration de la base de connaissances", - "xpack.observabilityAiAssistant.welcomeMessage.inspectErrorsButtonEmptyLabel": "Inspecter les problèmes", - "xpack.observabilityAiAssistant.welcomeMessage.issuesDescriptionListTitleLabel": "Problèmes", - "xpack.observabilityAiAssistant.welcomeMessage.knowledgeBaseSuccessfullyInstalledLabel": "La base de connaissances a été installée avec succès", - "xpack.observabilityAiAssistant.welcomeMessage.modelIsNotDeployedLabel": "Le modèle {modelName} n'est pas déployé", - "xpack.observabilityAiAssistant.welcomeMessage.modelIsNotFullyAllocatedLabel": "L'état d'allocation de {modelName} est {allocationState}", - "xpack.observabilityAiAssistant.welcomeMessage.modelIsNotStartedLabel": "L'état de déploiement de {modelName} est {deploymentState}", - "xpack.observabilityAiAssistant.welcomeMessage.retryButtonLabel": "Installer la base de connaissances", - "xpack.observabilityAiAssistant.welcomeMessage.trainedModelsLinkLabel": "Modèles entraînés", - "xpack.observabilityAiAssistant.welcomeMessage.weAreSettingUpTextLabel": "Nous configurons votre base de connaissances. Cette opération peut prendre quelques minutes. Vous pouvez continuer à utiliser l'Assistant lors de ce processus.", - "xpack.observabilityAiAssistant.welcomeMessageConnectors.connectorsErrorTextLabel": "Impossible de charger les connecteurs", - "xpack.observabilityAiAssistant.welcomeMessageConnectors.connectorsForbiddenTextLabel": "Vous n'avez pas les autorisations requises pour charger les connecteurs", - "xpack.observabilityAiAssistant.welcomeMessageKnowledgeBase.yourKnowledgeBaseIsNotSetUpCorrectlyLabel": "Votre base de connaissances n'a pas été configurée.", - "xpack.observabilityAiAssistant.welcomeMessageKnowledgeBaseSetupErrorPanel.retryInstallingLinkLabel": "Réessayer l'installation", "xpack.observabilityAiAssistantManagement.apmSettings.save.error": "Une erreur s'est produite lors de l'enregistrement des paramètres", "xpack.observabilityAiAssistantManagement.app.description": "Gérer l'Assistant d'IA pour Observability.", "xpack.observabilityAiAssistantManagement.app.title": "Assistant d'IA pour Observability", @@ -36151,8 +36123,6 @@ "xpack.securitySolution.detectionEngine.createRule.stepDefineRule.customThreatQueryFieldRequiredEmptyError": "Toutes les correspondances requièrent un champ et un champ d'index des menaces.", "xpack.securitySolution.detectionEngine.createRule.stepDefineRule.customThreatQueryFieldRequiredError": "Au moins une correspondance d'indicateur est requise.", "xpack.securitySolution.detectionEngine.createRule.stepDefineRule.dataViewSelectorFieldRequired": "Veuillez sélectionner une vue des données ou un modèle d'index disponible.", - "xpack.securitySolution.detectionEngine.createRule.stepDefineRule.enableThresholdSuppressionForFieldsLabel": "Supprimer les alertes par champs sélectionnés : {fieldsString} (version d'évaluation technique)", - "xpack.securitySolution.detectionEngine.createRule.stepDefineRule.enableThresholdSuppressionLabel": "Supprimer les alertes (version d'évaluation technique)", "xpack.securitySolution.detectionEngine.createRule.stepDefineRule.EqlQueryBarLabel": "Requête EQL", "xpack.securitySolution.detectionEngine.createRule.stepDefineRule.eqlQueryFieldRequiredError": "Une requête EQL est requise.", "xpack.securitySolution.detectionEngine.createRule.stepDefineRule.eqlSequenceSuppressionDisableText": "La suppression n'est pas prise en charge pour les requêtes de séquence EQL.", @@ -39635,18 +39605,13 @@ "xpack.securitySolution.notes.management.createdByColumnTitle": "Créé par", "xpack.securitySolution.notes.management.createdColumnTitle": "Créé", "xpack.securitySolution.notes.management.deleteAction": "Supprimer", - "xpack.securitySolution.notes.management.deleteDescription": "Supprimer cette note", "xpack.securitySolution.notes.management.deleteNotesCancel": "Annuler", "xpack.securitySolution.notes.management.deleteNotesConfirm": "Voulez-vous vraiment supprimer {selectedNotes} {selectedNotes, plural, one {note} other {notes}} ?", - "xpack.securitySolution.notes.management.deleteNotesModalTitle": "Supprimer les notes ?", "xpack.securitySolution.notes.management.deleteSelected": "Supprimer les notes sélectionnées", - "xpack.securitySolution.notes.management.eventIdColumnTitle": "Afficher le document", "xpack.securitySolution.notes.management.noteContentColumnTitle": "Contenu de la note", "xpack.securitySolution.notes.management.openTimeline": "Ouvrir la chronologie", "xpack.securitySolution.notes.management.refresh": "Actualiser", "xpack.securitySolution.notes.management.tableError": "Impossible de charger les notes", - "xpack.securitySolution.notes.management.timelineColumnTitle": "Chronologie", - "xpack.securitySolution.notes.management.viewEventInTimeline": "Afficher l'événement dans la chronologie", "xpack.securitySolution.notes.noteLabel": "Note", "xpack.securitySolution.notes.notesTitle": "Notes", "xpack.securitySolution.notes.search.FilterByUserOrNotePlaceholder": "Filtre par utilisateur ou note", diff --git a/x-pack/plugins/translations/translations/ja-JP.json b/x-pack/plugins/translations/translations/ja-JP.json index 3883a41164835..9361689702bb4 100644 --- a/x-pack/plugins/translations/translations/ja-JP.json +++ b/x-pack/plugins/translations/translations/ja-JP.json @@ -320,8 +320,6 @@ "coloring.dynamicColoring.rangeType.label": "値型", "coloring.dynamicColoring.rangeType.number": "Number", "coloring.dynamicColoring.rangeType.percent": "割合(%)", - "console.autocomplete.addMethodMetaText": "メソド", - "console.autocomplete.fieldsFetchingAnnotation": "フィールドの取得を実行しています", "console.autocompleteSuggestions.apiLabel": "API", "console.autocompleteSuggestions.endpointLabel": "エンドポイント", "console.autocompleteSuggestions.methodLabel": "メソド", @@ -362,10 +360,6 @@ "console.loadingError.title": "コンソールを読み込めません", "console.notification.clearHistory": "履歴を消去", "console.notification.disableSavingToHistory": "保存を無効にする", - "console.notification.error.couldNotSaveRequestTitle": "リクエストをコンソール履歴に保存できませんでした。", - "console.notification.error.historyQuotaReachedMessage": "リクエスト履歴が満杯です。コンソール履歴を消去するか、新しいリクエストの保存を無効にしてください。", - "console.notification.error.noRequestSelectedTitle": "リクエストを選択していません。リクエストの中にカーソルを置いて選択します。", - "console.notification.error.unknownErrorTitle": "不明なリクエストエラー", "console.pageHeading": "コンソール", "console.requestInProgressBadgeText": "リクエストが進行中", "console.requestOptions.autoIndentButtonLabel": "インデントを適用", @@ -7028,9 +7022,6 @@ "sharedUXPackages.fileUpload.uploadCompleteButtonLabel": "アップロード完了", "sharedUXPackages.fileUpload.uploadDoneToolTipContent": "ファイルは正常にアップロードされました。", "sharedUXPackages.fileUpload.uploadingButtonLabel": "アップロード中", - "sharedUXPackages.no_data_views.esqlButtonLabel": "言語:ES|QL", - "sharedUXPackages.no_data_views.esqlDocsLink": "詳細情報", - "sharedUXPackages.no_data_views.esqlMessage": "あるいは、直接ES|QLを使用してデータをクエリできます。{docsLink}", "sharedUXPackages.noDataConfig.addIntegrationsDescription": "Elasticエージェントを使用して、データを収集し、分析ソリューションを構築します。", "sharedUXPackages.noDataConfig.addIntegrationsTitle": "統合の追加", "sharedUXPackages.noDataConfig.analytics": "分析", @@ -7052,8 +7043,6 @@ "sharedUXPackages.noDataViewsPrompt.dataViewExplanation": "データビューは、探索するElasticsearchデータを特定します。昨日からのログデータ、ログデータを含むすべてのインデックスなど、1つ以上のデータストリーム、インデックス、インデックスエイリアスをデータビューで参照できます。", "sharedUXPackages.noDataViewsPrompt.learnMore": "詳細について", "sharedUXPackages.noDataViewsPrompt.noPermission.dataViewExplanation": "データビューは、探索するElasticsearchデータを特定します。データビューを作成するには、必要な権限を管理者に依頼してください。", - "sharedUXPackages.noDataViewsPrompt.noPermission.title": "データビューを作成するための権限が必要です。", - "sharedUXPackages.noDataViewsPrompt.nowCreate": "ここでデータビューを作成します。", "sharedUXPackages.noDataViewsPrompt.readDocumentation": "ドキュメントを読む", "sharedUXPackages.noDataViewsPrompt.youHaveData": "Elasticsearchにデータがあります。", "sharedUXPackages.prompt.errors.notFound.body": "申し訳ございません。お探しのページは見つかりませんでした。削除または名前変更されたか、そもそも存在していなかった可能性があります。", @@ -9141,6 +9130,94 @@ "xpack.actions.serverSideErrors.unavailableLicenseInformationErrorMessage": "グラフを利用できません。現在ライセンス情報が利用できません。", "xpack.actions.subActionsFramework.urlValidationError": "URLの検証エラー:{message}", "xpack.actions.urlAllowedHostsConfigurationError": "ターゲット{field}「{value}」はKibana構成xpack.actions.allowedHostsに追加されていません", + "xpack.aiAssistant.askAssistantButton.buttonLabel": "アシスタントに聞く", + "xpack.aiAssistant.askAssistantButton.popoverContent": "Elastic Assistantでデータに関するインサイトを得ましょう", + "xpack.aiAssistant.assistantSetup.title": "Elastic AI Assistantへようこそ", + "xpack.aiAssistant.chatActionsMenu.euiButtonIcon.menuLabel": "メニュー", + "xpack.aiAssistant.chatActionsMenu.euiToolTip.moreActionsLabel": "さらにアクションを表示", + "xpack.aiAssistant.chatCollapsedItems.hideEvents": "{count}件のイベントを非表示", + "xpack.aiAssistant.chatCollapsedItems.showEvents": "{count}件のイベントを表示", + "xpack.aiAssistant.chatCollapsedItems.toggleButtonLabel": "アイテムを表示/非表示", + "xpack.aiAssistant.chatFlyout.euiButtonIcon.expandConversationListLabel": "会話リストを展開", + "xpack.aiAssistant.chatFlyout.euiButtonIcon.newChatLabel": "新しいチャット", + "xpack.aiAssistant.chatFlyout.euiToolTip.collapseConversationListLabel": "会話リストを折りたたむ", + "xpack.aiAssistant.chatFlyout.euiToolTip.expandConversationListLabel": "会話リストを展開", + "xpack.aiAssistant.chatFlyout.euiToolTip.newChatLabel": "新しいチャット", + "xpack.aiAssistant.chatHeader.actions.connector": "コネクター", + "xpack.aiAssistant.chatHeader.actions.copyConversation": "会話をコピー", + "xpack.aiAssistant.chatHeader.actions.knowledgeBase": "ナレッジベースを管理", + "xpack.aiAssistant.chatHeader.actions.settings": "AI Assistant設定", + "xpack.aiAssistant.chatHeader.actions.title": "アクション", + "xpack.aiAssistant.chatHeader.editConversationInput": "会話を編集", + "xpack.aiAssistant.chatHeader.euiButtonIcon.navigateToConversationsLabel": "会話に移動", + "xpack.aiAssistant.chatHeader.euiButtonIcon.toggleFlyoutModeLabel": "フライアウトモードを切り替え", + "xpack.aiAssistant.chatHeader.euiToolTip.flyoutModeLabel.dock": "チャットを固定", + "xpack.aiAssistant.chatHeader.euiToolTip.flyoutModeLabel.undock": "チャットを固定解除", + "xpack.aiAssistant.chatHeader.euiToolTip.navigateToConversationsLabel": "会話に移動", + "xpack.aiAssistant.chatPromptEditor.codeEditor.payloadEditorLabel": "payloadEditor", + "xpack.aiAssistant.chatPromptEditor.euiButtonIcon.submitLabel": "送信", + "xpack.aiAssistant.chatTimeline.actions.copyMessage": "メッセージをコピー", + "xpack.aiAssistant.chatTimeline.actions.copyMessageSuccessful": "コピーされたメッセージ", + "xpack.aiAssistant.chatTimeline.actions.editPrompt": "プロンプトを編集", + "xpack.aiAssistant.chatTimeline.actions.inspectPrompt": "プロンプトを検査", + "xpack.aiAssistant.chatTimeline.messages.elasticAssistant.label": "Elastic Assistant", + "xpack.aiAssistant.chatTimeline.messages.system.label": "システム", + "xpack.aiAssistant.chatTimeline.messages.user.label": "あなた", + "xpack.aiAssistant.checkingKbAvailability": "ナレッジベースの利用可能性を確認中", + "xpack.aiAssistant.conversationStartTitle": "会話を開始しました", + "xpack.aiAssistant.couldNotFindConversationContent": "id {conversationId}の会話が見つかりませんでした。会話が存在し、それにアクセスできることを確認してください。", + "xpack.aiAssistant.couldNotFindConversationTitle": "会話が見つかりません", + "xpack.aiAssistant.disclaimer.disclaimerLabel": "このチャットは、LLMプロバイダーとの統合によって提供されています。LLMは、正しくない情報を正しい情報であるかのように表示する場合があることが知られています。Elasticは、構成やLLMプロバイダーへの接続、お客様のナレッジベースへの接続はサポートしますが、LLMの応答については責任を負いません。", + "xpack.aiAssistant.emptyConversationTitle": "新しい会話", + "xpack.aiAssistant.errorSettingUpKnowledgeBase": "ナレッジベースをセットアップできませんでした", + "xpack.aiAssistant.errorUpdatingConversation": "会話を更新できませんでした", + "xpack.aiAssistant.executedFunctionFailureEvent": "関数{functionName}の実行に失敗しました", + "xpack.aiAssistant.failedToGetStatus": "モデルステータスを取得できませんでした。", + "xpack.aiAssistant.failedToSetupKnowledgeBase": "ナレッジベースをセットアップできませんでした。", + "xpack.aiAssistant.flyout.confirmDeleteButtonText": "会話を削除", + "xpack.aiAssistant.flyout.confirmDeleteConversationContent": "この操作は元に戻すことができません。", + "xpack.aiAssistant.flyout.confirmDeleteConversationTitle": "この会話を削除しますか?", + "xpack.aiAssistant.flyout.failedToDeleteConversation": "会話を削除できませんでした", + "xpack.aiAssistant.functionListPopover.euiButtonIcon.selectAFunctionLabel": "関数を選択", + "xpack.aiAssistant.functionListPopover.euiToolTip.clearFunction": "関数を消去", + "xpack.aiAssistant.functionListPopover.euiToolTip.selectAFunctionLabel": "関数を選択", + "xpack.aiAssistant.hideExpandConversationButton.hide": "チャットを非表示", + "xpack.aiAssistant.hideExpandConversationButton.show": "チャットを表示", + "xpack.aiAssistant.incorrectLicense.body": "Elastic AI Assistantを使用するにはEnterpriseライセンスが必要です。", + "xpack.aiAssistant.incorrectLicense.manageLicense": "ライセンスの管理", + "xpack.aiAssistant.incorrectLicense.subscriptionPlansButton": "サブスクリプションオプション", + "xpack.aiAssistant.incorrectLicense.title": "ライセンスをアップグレード", + "xpack.aiAssistant.initialSetupPanel.setupConnector.buttonLabel": "GenAIコネクターをセットアップ", + "xpack.aiAssistant.initialSetupPanel.setupConnector.description2": "Elastic AI Assistantの使用を開始するには、AIプロバイダーのコネクターを設定します。モデルは関数呼び出しをサポートしている必要があります。OpenAIまたはAzureを使用するときには、GPT4を使用することをお勧めします。", + "xpack.aiAssistant.installingKb": "ナレッジベースをセットアップ中", + "xpack.aiAssistant.newChatButton": "新しいチャット", + "xpack.aiAssistant.poweredByModel": "{model}で駆動", + "xpack.aiAssistant.prompt.functionList.filter": "フィルター", + "xpack.aiAssistant.prompt.functionList.functionList": "関数リスト", + "xpack.aiAssistant.prompt.placeholder": "アシスタントにメッセージを送信", + "xpack.aiAssistant.promptEditorNaturalLanguage.euiSelectable.selectAnOptionLabel": "オプションを選択", + "xpack.aiAssistant.settingsPage.goToConnectorsButtonLabel": "コネクターを管理", + "xpack.aiAssistant.setupKb": "ナレッジベースを設定することで、エクスペリエンスが改善されます。", + "xpack.aiAssistant.simulatedFunctionCallingCalloutLabel": "シミュレートされた関数呼び出しが有効です。パフォーマンスが劣化する場合があります。", + "xpack.aiAssistant.suggestedFunctionEvent": "関数{functionName}を要求しました", + "xpack.aiAssistant.technicalPreviewBadgeDescription": "関数呼び出し(根本原因分析やデータの視覚化など)を使用する際に、より一貫性のあるエクスペリエンスを実現するために、GPT4が必要です。GPT3.5は、エラーの説明などのシンプルなワークフローの一部や、頻繁な関数呼び出しの使用が必要とされないKibana内のエクスペリエンスなどのChatGPTで機能します。", + "xpack.aiAssistant.userExecutedFunctionEvent": "関数{functionName}を実行しました", + "xpack.aiAssistant.userSuggestedFunctionEvent": "関数{functionName}を要求しました", + "xpack.aiAssistant.welcomeMessage.div.checkTrainedModelsToLabel": " {retryInstallingLink}か、{trainedModelsLink}を確認して、{modelName}がデプロイされ、実行中であることを確かめてください。", + "xpack.aiAssistant.welcomeMessage.div.settingUpKnowledgeBaseLabel": "ナレッジベースをセットアップ中", + "xpack.aiAssistant.welcomeMessage.inspectErrorsButtonEmptyLabel": "問題を検査", + "xpack.aiAssistant.welcomeMessage.issuesDescriptionListTitleLabel": "問題", + "xpack.aiAssistant.welcomeMessage.knowledgeBaseSuccessfullyInstalledLabel": "ナレッジベースは正常にインストールされました", + "xpack.aiAssistant.welcomeMessage.modelIsNotDeployedLabel": "モデル\"{modelName}\"はデプロイされていません", + "xpack.aiAssistant.welcomeMessage.modelIsNotFullyAllocatedLabel": "\"{modelName}\"の割り当て状態は{allocationState}です", + "xpack.aiAssistant.welcomeMessage.modelIsNotStartedLabel": "\"{modelName}\"のデプロイ状態は{deploymentState}です", + "xpack.aiAssistant.welcomeMessage.retryButtonLabel": "ナレッジベースをインストール", + "xpack.aiAssistant.welcomeMessage.trainedModelsLinkLabel": "学習済みモデル", + "xpack.aiAssistant.welcomeMessage.weAreSettingUpTextLabel": "ナレッジベースをセットアップしています。これには数分かかる場合があります。この処理の実行中には、アシスタントを使用し続けることができます。", + "xpack.aiAssistant.welcomeMessageConnectors.connectorsErrorTextLabel": "コネクターを読み込めませんでした", + "xpack.aiAssistant.welcomeMessageConnectors.connectorsForbiddenTextLabel": "コネクターを取得するために必要な権限が不足しています", + "xpack.aiAssistant.welcomeMessageKnowledgeBase.yourKnowledgeBaseIsNotSetUpCorrectlyLabel": "ナレッジベースはセットアップされていません。", + "xpack.aiAssistant.welcomeMessageKnowledgeBaseSetupErrorPanel.retryInstallingLinkLabel": "インストールを再試行", "xpack.aiops.actions.openChangePointInMlAppName": "AIOps Labsで開く", "xpack.aiops.analysis.columnSelectorAriaLabel": "列のフィルタリング", "xpack.aiops.analysis.columnSelectorNotEnoughColumnsSelected": "1つ以上の列を選択する必要があります。", @@ -22963,7 +23040,6 @@ "xpack.infra.logs.highlights.highlightTermsFieldLabel": "ハイライトする用語", "xpack.infra.logs.index.anomaliesTabTitle": "異常", "xpack.infra.logs.index.logCategoriesBetaBadgeTitle": "カテゴリー", - "xpack.infra.logs.index.logsLabel": "ログ", "xpack.infra.logs.index.settingsTabTitle": "設定", "xpack.infra.logs.index.streamTabTitle": "ストリーム", "xpack.infra.logs.logCategoriesTitle": "カテゴリー", @@ -28391,7 +28467,6 @@ "xpack.ml.jobsList.jobDetails.forecastsTable.processingTimeLabel": "処理時間", "xpack.ml.jobsList.jobDetails.forecastsTable.statusLabel": "ステータス", "xpack.ml.jobsList.jobDetails.forecastsTable.toLabel": "終了:", - "xpack.ml.jobsList.jobDetails.forecastsTable.viewAriaLabel": "{createdDate} に作成された予測を表示", "xpack.ml.jobsList.jobDetails.forecastsTable.viewLabel": "表示", "xpack.ml.jobsList.jobDetails.generalTitle": "一般", "xpack.ml.jobsList.jobDetails.influencersTitle": "影響", @@ -29818,7 +29893,6 @@ "xpack.ml.trainedModels.modelsList.builtInModelLabel": "ビルトイン", "xpack.ml.trainedModels.modelsList.builtInModelMessage": "ビルトインモデル", "xpack.ml.trainedModels.modelsList.collapseRow": "縮小", - "xpack.ml.trainedModels.modelsList.createdAtHeader": "作成日時:", "xpack.ml.trainedModels.modelsList.deleteDisabledWithDeploymentsTooltip": "モデルはデプロイを開始しました", "xpack.ml.trainedModels.modelsList.deleteDisabledWithInferenceServicesTooltip": "モデルは_inference APIによって使用されます。", "xpack.ml.trainedModels.modelsList.deleteModal.approvePipelinesDeletionLabel": "{pipelinesCount, plural, other {パイプライン}}を削除", @@ -29833,9 +29907,7 @@ "xpack.ml.trainedModels.modelsList.deleteModelsButtonLabel": "削除", "xpack.ml.trainedModels.modelsList.deployModelActionLabel": "モデルをデプロイ", "xpack.ml.trainedModels.modelsList.disableSelectableMessage": "モデルにはパイプラインが関連付けられています", - "xpack.ml.trainedModels.modelsList.downloadCompleteSuccess": "\"{modelIds}\" {modelIdsLength, plural, other {が}}正常にダウンロードされました。", "xpack.ml.trainedModels.modelsList.downloadFailed": "\"{modelId}\"をダウンロードできませんでした", - "xpack.ml.trainedModels.modelsList.downloadStatusCheckErrorMessage": "ダウンロードステータスを確認できませんでした", "xpack.ml.trainedModels.modelsList.e5Title": "E5(bidirEctional Encoder rEpresentationsからのEmbEddings)", "xpack.ml.trainedModels.modelsList.e5v1Description": "E5(bidirEctional Encoder rEpresentationsからのEmbEddings)", "xpack.ml.trainedModels.modelsList.e5v1x86Description": "E5(bidirEctional Encoder rEpresentationsからのEmbEddings)、inux-x86_64向けに最適化", @@ -29873,11 +29945,9 @@ "xpack.ml.trainedModels.modelsList.forceStopDialog.selectDeploymentsLegend": "停止するデプロイを選択", "xpack.ml.trainedModels.modelsList.forceStopDialog.title": "モデル{modelId}の{deploymentCount, plural, other {デプロイ}}を停止しますか?", "xpack.ml.trainedModels.modelsList.mitLicenseLabel": "ライセンス:MIT", - "xpack.ml.trainedModels.modelsList.modelDescriptionHeader": "説明", "xpack.ml.trainedModels.modelsList.modelIdHeader": "ID", "xpack.ml.trainedModels.modelsList.modelState.downloadedName": "デプロイできます", "xpack.ml.trainedModels.modelsList.modelState.downloadingName": "ダウンロード中...", - "xpack.ml.trainedModels.modelsList.modelState.notDownloadedName": "未ダウンロード", "xpack.ml.trainedModels.modelsList.modelState.startedName": "デプロイ済み", "xpack.ml.trainedModels.modelsList.modelState.startingName": "デプロイを開始中...", "xpack.ml.trainedModels.modelsList.modelState.stoppingName": "デプロイを停止中...", @@ -29904,13 +29974,10 @@ "xpack.ml.trainedModels.modelsList.startDeployment.updateButton": "更新", "xpack.ml.trainedModels.modelsList.startDeployment.viewElserDocLink": "ドキュメンテーションを表示", "xpack.ml.trainedModels.modelsList.startFailed": "\"{modelId}\"の開始に失敗しました", - "xpack.ml.trainedModels.modelsList.startSuccess": "\"{modelId}\"のデプロイが正常に開始しました。", "xpack.ml.trainedModels.modelsList.stateHeader": "ステータス", "xpack.ml.trainedModels.modelsList.stopDeploymentWarning": "\"{deploymentId}\"を停止できませんでした", "xpack.ml.trainedModels.modelsList.stopFailed": "\"{modelId}\"の停止に失敗しました", - "xpack.ml.trainedModels.modelsList.stopSuccess": "\"{modelId}\"の{numberOfDeployments, plural, other {デプロイ}}が正常に停止しました。", "xpack.ml.trainedModels.modelsList.totalAmountLabel": "学習済みモデルの合計数", - "xpack.ml.trainedModels.modelsList.typeHeader": "型", "xpack.ml.trainedModels.modelsList.updateDeployment.modalTitle": "{modelId}デプロイを更新", "xpack.ml.trainedModels.modelsList.updateFailed": "\"{modelId}\"を更新できませんでした", "xpack.ml.trainedModels.modelsList.updateSuccess": "\"{modelId}\"のデプロイが正常に更新されました。", @@ -32310,92 +32377,27 @@ "xpack.observabilityAiAssistant.app.starterPrompts.whatAreSlos.prompt": "SLOとは何ですか?", "xpack.observabilityAiAssistant.app.starterPrompts.whatAreSlos.title": "SLO", "xpack.observabilityAiAssistant.appTitle": "オブザーバビリティAI Assistant", - "xpack.observabilityAiAssistant.askAssistantButton.buttonLabel": "アシスタントに聞く", - "xpack.observabilityAiAssistant.askAssistantButton.popoverContent": "Elastic Assistantでデータに関するインサイトを得ましょう", - "xpack.observabilityAiAssistant.askAssistantButton.popoverTitle": "AI Assistant for Observability", - "xpack.observabilityAiAssistant.assistantSetup.title": "Elastic AI Assistantへようこそ", "xpack.observabilityAiAssistant.changesList.dotImpactHigh": "高", "xpack.observabilityAiAssistant.changesList.dotImpactLow": "低", "xpack.observabilityAiAssistant.changesList.dotImpactMedium": "中", "xpack.observabilityAiAssistant.changesList.labelColumnTitle": "ラベル", "xpack.observabilityAiAssistant.changesList.noChangesDetected": "変更が検出されません", "xpack.observabilityAiAssistant.changesList.trendColumnTitle": "傾向", - "xpack.observabilityAiAssistant.chatActionsMenu.euiButtonIcon.menuLabel": "メニュー", - "xpack.observabilityAiAssistant.chatActionsMenu.euiToolTip.moreActionsLabel": "さらにアクションを表示", - "xpack.observabilityAiAssistant.chatCollapsedItems.hideEvents": "{count}件のイベントを非表示", - "xpack.observabilityAiAssistant.chatCollapsedItems.showEvents": "{count}件のイベントを表示", - "xpack.observabilityAiAssistant.chatCollapsedItems.toggleButtonLabel": "アイテムを表示/非表示", "xpack.observabilityAiAssistant.chatCompletionError.conversationNotFoundError": "会話が見つかりません", "xpack.observabilityAiAssistant.chatCompletionError.internalServerError": "内部サーバーエラーが発生しました", "xpack.observabilityAiAssistant.chatCompletionError.tokenLimitReachedError": "トークンの上限に達しました。トークンの上限は{tokenLimit}ですが、現在の会話には{tokenCount}個のトークンがあります。", - "xpack.observabilityAiAssistant.chatFlyout.euiButtonIcon.expandConversationListLabel": "会話リストを展開", - "xpack.observabilityAiAssistant.chatFlyout.euiButtonIcon.newChatLabel": "新しいチャット", - "xpack.observabilityAiAssistant.chatFlyout.euiFlyoutResizable.aiAssistantForObservabilityLabel": "AI assistant for Observabilityチャットフライアウト", - "xpack.observabilityAiAssistant.chatFlyout.euiToolTip.collapseConversationListLabel": "会話リストを折りたたむ", - "xpack.observabilityAiAssistant.chatFlyout.euiToolTip.expandConversationListLabel": "会話リストを展開", - "xpack.observabilityAiAssistant.chatFlyout.euiToolTip.newChatLabel": "新しいチャット", - "xpack.observabilityAiAssistant.chatHeader.actions.connector": "コネクター", - "xpack.observabilityAiAssistant.chatHeader.actions.copyConversation": "会話をコピー", - "xpack.observabilityAiAssistant.chatHeader.actions.knowledgeBase": "ナレッジベースを管理", - "xpack.observabilityAiAssistant.chatHeader.actions.settings": "AI Assistant設定", - "xpack.observabilityAiAssistant.chatHeader.actions.title": "アクション", - "xpack.observabilityAiAssistant.chatHeader.editConversationInput": "会話を編集", - "xpack.observabilityAiAssistant.chatHeader.euiButtonIcon.navigateToConversationsLabel": "会話に移動", - "xpack.observabilityAiAssistant.chatHeader.euiButtonIcon.toggleFlyoutModeLabel": "フライアウトモードを切り替え", - "xpack.observabilityAiAssistant.chatHeader.euiToolTip.flyoutModeLabel.dock": "チャットを固定", - "xpack.observabilityAiAssistant.chatHeader.euiToolTip.flyoutModeLabel.undock": "チャットを固定解除", - "xpack.observabilityAiAssistant.chatHeader.euiToolTip.navigateToConversationsLabel": "会話に移動", - "xpack.observabilityAiAssistant.chatPromptEditor.codeEditor.payloadEditorLabel": "payloadEditor", - "xpack.observabilityAiAssistant.chatPromptEditor.euiButtonIcon.submitLabel": "送信", "xpack.observabilityAiAssistant.chatService.div.helloLabel": "こんにちは", - "xpack.observabilityAiAssistant.chatTimeline.actions.copyMessage": "メッセージをコピー", - "xpack.observabilityAiAssistant.chatTimeline.actions.copyMessageSuccessful": "コピーされたメッセージ", - "xpack.observabilityAiAssistant.chatTimeline.actions.editPrompt": "プロンプトを編集", - "xpack.observabilityAiAssistant.chatTimeline.actions.inspectPrompt": "プロンプトを検査", - "xpack.observabilityAiAssistant.chatTimeline.messages.elasticAssistant.label": "Elastic Assistant", - "xpack.observabilityAiAssistant.chatTimeline.messages.system.label": "システム", - "xpack.observabilityAiAssistant.chatTimeline.messages.user.label": "あなた", - "xpack.observabilityAiAssistant.checkingKbAvailability": "ナレッジベースの利用可能性を確認中", "xpack.observabilityAiAssistant.connectorSelector.connectorSelectLabel": "コネクター:", "xpack.observabilityAiAssistant.connectorSelector.empty": "コネクターなし", "xpack.observabilityAiAssistant.connectorSelector.error": "コネクターを読み込めませんでした", - "xpack.observabilityAiAssistant.conversationList.deleteConversationIconLabel": "削除", - "xpack.observabilityAiAssistant.conversationList.errorMessage": "の読み込みに失敗しました", - "xpack.observabilityAiAssistant.conversationList.noConversations": "会話なし", - "xpack.observabilityAiAssistant.conversationList.title": "以前", "xpack.observabilityAiAssistant.conversationsDeepLinkTitle": "会話", - "xpack.observabilityAiAssistant.conversationStartTitle": "会話を開始しました", - "xpack.observabilityAiAssistant.couldNotFindConversationContent": "id {conversationId}の会話が見つかりませんでした。会話が存在し、それにアクセスできることを確認してください。", - "xpack.observabilityAiAssistant.couldNotFindConversationTitle": "会話が見つかりません", - "xpack.observabilityAiAssistant.disclaimer.disclaimerLabel": "このチャットは、LLMプロバイダーとの統合によって提供されています。LLMは、正しくない情報を正しい情報であるかのように表示する場合があることが知られています。Elasticは、構成やLLMプロバイダーへの接続、お客様のナレッジベースへの接続はサポートしますが、LLMの応答については責任を負いません。", - "xpack.observabilityAiAssistant.emptyConversationTitle": "新しい会話", - "xpack.observabilityAiAssistant.errorSettingUpKnowledgeBase": "ナレッジベースをセットアップできませんでした", - "xpack.observabilityAiAssistant.errorUpdatingConversation": "会話を更新できませんでした", - "xpack.observabilityAiAssistant.executedFunctionFailureEvent": "関数{functionName}の実行に失敗しました", "xpack.observabilityAiAssistant.experimentalTitle": "テクニカルプレビュー", "xpack.observabilityAiAssistant.experimentalTooltip": "この機能はテクニカルプレビュー中であり、将来のリリースでは変更されたり完全に削除されたりする場合があります。Elasticはすべての問題の修正に努めますが、テクニカルプレビュー中の機能には正式なGA機能のサポートSLAが適用されません。", "xpack.observabilityAiAssistant.failedLoadingResponseText": "応答の読み込みに失敗しました", - "xpack.observabilityAiAssistant.failedToGetStatus": "モデルステータスを取得できませんでした。", "xpack.observabilityAiAssistant.failedToLoadResponse": "AIアシスタントからの応答を読み込めませんでした", - "xpack.observabilityAiAssistant.failedToSetupKnowledgeBase": "ナレッジベースをセットアップできませんでした。", "xpack.observabilityAiAssistant.featureRegistry.featureName": "オブザーバビリティAI Assistant", "xpack.observabilityAiAssistant.feedbackButtons.em.thanksForYourFeedbackLabel": "フィードバックをご提供いただき、ありがとうございました。", - "xpack.observabilityAiAssistant.flyout.confirmDeleteButtonText": "会話を削除", - "xpack.observabilityAiAssistant.flyout.confirmDeleteConversationContent": "この操作は元に戻すことができません。", - "xpack.observabilityAiAssistant.flyout.confirmDeleteConversationTitle": "この会話を削除しますか?", - "xpack.observabilityAiAssistant.flyout.failedToDeleteConversation": "会話を削除できませんでした", "xpack.observabilityAiAssistant.functionCallLimitExceeded": "\n\n注:Assistantは、上限を超過しても、関数を呼び出そうとします。", - "xpack.observabilityAiAssistant.functionListPopover.euiButtonIcon.selectAFunctionLabel": "関数を選択", - "xpack.observabilityAiAssistant.functionListPopover.euiToolTip.clearFunction": "関数を消去", - "xpack.observabilityAiAssistant.functionListPopover.euiToolTip.selectAFunctionLabel": "関数を選択", - "xpack.observabilityAiAssistant.hideExpandConversationButton.hide": "チャットを非表示", - "xpack.observabilityAiAssistant.hideExpandConversationButton.show": "チャットを表示", - "xpack.observabilityAiAssistant.incorrectLicense.body": "Elastic AI Assistantを使用するにはEnterpriseライセンスが必要です。", - "xpack.observabilityAiAssistant.incorrectLicense.manageLicense": "ライセンスの管理", - "xpack.observabilityAiAssistant.incorrectLicense.subscriptionPlansButton": "サブスクリプションオプション", - "xpack.observabilityAiAssistant.incorrectLicense.title": "ライセンスをアップグレード", - "xpack.observabilityAiAssistant.initialSetupPanel.setupConnector.buttonLabel": "GenAIコネクターをセットアップ", - "xpack.observabilityAiAssistant.initialSetupPanel.setupConnector.description2": "Elastic AI Assistantの使用を開始するには、AIプロバイダーのコネクターを設定します。モデルは関数呼び出しをサポートしている必要があります。OpenAIまたはAzureを使用するときには、GPT4を使用することをお勧めします。", "xpack.observabilityAiAssistant.insight.actions": "アクション", "xpack.observabilityAiAssistant.insight.actions.connector": "コネクター", "xpack.observabilityAiAssistant.insight.actions.editPrompt": "プロンプトを編集", @@ -32413,7 +32415,6 @@ "xpack.observabilityAiAssistant.insight.response.startChat": "チャットを開始", "xpack.observabilityAiAssistant.insight.sendPromptEdit": "プロンプトを送信", "xpack.observabilityAiAssistant.insightModifiedPrompt": "このインサイトは修正されました。", - "xpack.observabilityAiAssistant.installingKb": "ナレッジベースをセットアップ中", "xpack.observabilityAiAssistant.lensESQLFunction.displayChart": "グラフを表示", "xpack.observabilityAiAssistant.lensESQLFunction.displayTable": "表を表示", "xpack.observabilityAiAssistant.lensESQLFunction.edit": "ビジュアライゼーションを編集", @@ -32428,42 +32429,14 @@ "xpack.observabilityAiAssistant.missingCredentialsCallout.title": "資格情報がありません", "xpack.observabilityAiAssistant.navControl.initFailureErrorTitle": "オブザーバビリティAI Assistantを初期化できませんでした", "xpack.observabilityAiAssistant.navControl.openTheAIAssistantPopoverLabel": "AI Assistantを開く", - "xpack.observabilityAiAssistant.newChatButton": "新しいチャット", - "xpack.observabilityAiAssistant.poweredByModel": "{model}で駆動", - "xpack.observabilityAiAssistant.prompt.functionList.filter": "フィルター", - "xpack.observabilityAiAssistant.prompt.functionList.functionList": "関数リスト", - "xpack.observabilityAiAssistant.prompt.placeholder": "アシスタントにメッセージを送信", - "xpack.observabilityAiAssistant.promptEditorNaturalLanguage.euiSelectable.selectAnOptionLabel": "オプションを選択", "xpack.observabilityAiAssistant.regenerateResponseButtonLabel": "再生成", "xpack.observabilityAiAssistant.requiredConnectorField": "コネクターが必要です。", "xpack.observabilityAiAssistant.requiredMessageTextField": "メッセージが必要です。", "xpack.observabilityAiAssistant.resetDefaultPrompt": "デフォルトにリセット", "xpack.observabilityAiAssistant.runThisQuery": "結果を表示", - "xpack.observabilityAiAssistant.settingsPage.goToConnectorsButtonLabel": "コネクターを管理", - "xpack.observabilityAiAssistant.setupKb": "ナレッジベースを設定することで、エクスペリエンスが改善されます。", - "xpack.observabilityAiAssistant.simulatedFunctionCallingCalloutLabel": "シミュレートされた関数呼び出しが有効です。パフォーマンスが劣化する場合があります。", "xpack.observabilityAiAssistant.stopGeneratingButtonLabel": "生成を停止", - "xpack.observabilityAiAssistant.suggestedFunctionEvent": "関数{functionName}を要求しました", - "xpack.observabilityAiAssistant.technicalPreviewBadgeDescription": "関数呼び出し(根本原因分析やデータの視覚化など)を使用する際に、より一貫性のあるエクスペリエンスを実現するために、GPT4が必要です。GPT3.5は、エラーの説明などのシンプルなワークフローの一部や、頻繁な関数呼び出しの使用が必要とされないKibana内のエクスペリエンスなどのChatGPTで機能します。", "xpack.observabilityAiAssistant.tokenLimitError": "会話はトークンの上限を超えました。トークンの最大上限は**{tokenLimit}**ですが、現在の会話には**{tokenCount}**個のトークンがあります。続行するには、新しい会話を開始してください。", - "xpack.observabilityAiAssistant.userExecutedFunctionEvent": "関数{functionName}を実行しました", - "xpack.observabilityAiAssistant.userSuggestedFunctionEvent": "関数{functionName}を要求しました", "xpack.observabilityAiAssistant.visualizeThisQuery": "このクエリーを可視化", - "xpack.observabilityAiAssistant.welcomeMessage.div.checkTrainedModelsToLabel": " {retryInstallingLink}か、{trainedModelsLink}を確認して、{modelName}がデプロイされ、実行中であることを確かめてください。", - "xpack.observabilityAiAssistant.welcomeMessage.div.settingUpKnowledgeBaseLabel": "ナレッジベースをセットアップ中", - "xpack.observabilityAiAssistant.welcomeMessage.inspectErrorsButtonEmptyLabel": "問題を検査", - "xpack.observabilityAiAssistant.welcomeMessage.issuesDescriptionListTitleLabel": "問題", - "xpack.observabilityAiAssistant.welcomeMessage.knowledgeBaseSuccessfullyInstalledLabel": "ナレッジベースは正常にインストールされました", - "xpack.observabilityAiAssistant.welcomeMessage.modelIsNotDeployedLabel": "モデル\"{modelName}\"はデプロイされていません", - "xpack.observabilityAiAssistant.welcomeMessage.modelIsNotFullyAllocatedLabel": "\"{modelName}\"の割り当て状態は{allocationState}です", - "xpack.observabilityAiAssistant.welcomeMessage.modelIsNotStartedLabel": "\"{modelName}\"のデプロイ状態は{deploymentState}です", - "xpack.observabilityAiAssistant.welcomeMessage.retryButtonLabel": "ナレッジベースをインストール", - "xpack.observabilityAiAssistant.welcomeMessage.trainedModelsLinkLabel": "学習済みモデル", - "xpack.observabilityAiAssistant.welcomeMessage.weAreSettingUpTextLabel": "ナレッジベースをセットアップしています。これには数分かかる場合があります。この処理の実行中には、アシスタントを使用し続けることができます。", - "xpack.observabilityAiAssistant.welcomeMessageConnectors.connectorsErrorTextLabel": "コネクターを読み込めませんでした", - "xpack.observabilityAiAssistant.welcomeMessageConnectors.connectorsForbiddenTextLabel": "コネクターを取得するために必要な権限が不足しています", - "xpack.observabilityAiAssistant.welcomeMessageKnowledgeBase.yourKnowledgeBaseIsNotSetUpCorrectlyLabel": "ナレッジベースはセットアップされていません。", - "xpack.observabilityAiAssistant.welcomeMessageKnowledgeBaseSetupErrorPanel.retryInstallingLinkLabel": "インストールを再試行", "xpack.observabilityAiAssistantManagement.apmSettings.save.error": "設定の保存中にエラーが発生しました", "xpack.observabilityAiAssistantManagement.app.description": "AI Assistant for Observabilityを管理します。", "xpack.observabilityAiAssistantManagement.app.title": "AI Assistant for Observability", @@ -35894,8 +35867,6 @@ "xpack.securitySolution.detectionEngine.createRule.stepDefineRule.customThreatQueryFieldRequiredEmptyError": "すべての一致には、フィールドと脅威インデックスフィールドの両方が必要です。", "xpack.securitySolution.detectionEngine.createRule.stepDefineRule.customThreatQueryFieldRequiredError": "1 つ以上のインジケーター一致が必要です。", "xpack.securitySolution.detectionEngine.createRule.stepDefineRule.dataViewSelectorFieldRequired": "使用可能なデータビューまたはインデックスパターンを選択してください。", - "xpack.securitySolution.detectionEngine.createRule.stepDefineRule.enableThresholdSuppressionForFieldsLabel": "選択したフィールドでアラートを非表示:{fieldsString}(テクニカルプレビュー)", - "xpack.securitySolution.detectionEngine.createRule.stepDefineRule.enableThresholdSuppressionLabel": "アラートを抑制(テクニカルプレビュー)", "xpack.securitySolution.detectionEngine.createRule.stepDefineRule.EqlQueryBarLabel": "EQL クエリ", "xpack.securitySolution.detectionEngine.createRule.stepDefineRule.eqlQueryFieldRequiredError": "EQLクエリは必須です。", "xpack.securitySolution.detectionEngine.createRule.stepDefineRule.eqlSequenceSuppressionDisableText": "EQLシーケンスクエリでは抑制はサポートされていません。", @@ -39378,18 +39349,13 @@ "xpack.securitySolution.notes.management.createdByColumnTitle": "作成者", "xpack.securitySolution.notes.management.createdColumnTitle": "作成済み", "xpack.securitySolution.notes.management.deleteAction": "削除", - "xpack.securitySolution.notes.management.deleteDescription": "このメモを削除", "xpack.securitySolution.notes.management.deleteNotesCancel": "キャンセル", "xpack.securitySolution.notes.management.deleteNotesConfirm": "{selectedNotes} {selectedNotes, plural, other {件のメモ}}を削除しますか?", - "xpack.securitySolution.notes.management.deleteNotesModalTitle": "メモを削除しますか?", "xpack.securitySolution.notes.management.deleteSelected": "選択したメモを削除", - "xpack.securitySolution.notes.management.eventIdColumnTitle": "ドキュメンテーションを表示", "xpack.securitySolution.notes.management.noteContentColumnTitle": "メモコンテンツ", "xpack.securitySolution.notes.management.openTimeline": "タイムラインを開く", "xpack.securitySolution.notes.management.refresh": "更新", "xpack.securitySolution.notes.management.tableError": "メモを読み込めません", - "xpack.securitySolution.notes.management.timelineColumnTitle": "Timeline", - "xpack.securitySolution.notes.management.viewEventInTimeline": "タイムラインでイベントを表示", "xpack.securitySolution.notes.noteLabel": "注", "xpack.securitySolution.notes.notesTitle": "メモ", "xpack.securitySolution.notes.search.FilterByUserOrNotePlaceholder": "ユーザーまたはメモでフィルター", diff --git a/x-pack/plugins/translations/translations/zh-CN.json b/x-pack/plugins/translations/translations/zh-CN.json index 823092906f7ba..0a17edfeb80ce 100644 --- a/x-pack/plugins/translations/translations/zh-CN.json +++ b/x-pack/plugins/translations/translations/zh-CN.json @@ -319,8 +319,6 @@ "coloring.dynamicColoring.rangeType.label": "值类型", "coloring.dynamicColoring.rangeType.number": "数字", "coloring.dynamicColoring.rangeType.percent": "百分比", - "console.autocomplete.addMethodMetaText": "方法", - "console.autocomplete.fieldsFetchingAnnotation": "正在提取字段", "console.autocompleteSuggestions.apiLabel": "API", "console.autocompleteSuggestions.endpointLabel": "终端", "console.autocompleteSuggestions.methodLabel": "方法", @@ -361,10 +359,6 @@ "console.loadingError.title": "无法加载控制台", "console.notification.clearHistory": "清除历史记录", "console.notification.disableSavingToHistory": "禁止保存", - "console.notification.error.couldNotSaveRequestTitle": "无法将请求保存到控制台历史记录。", - "console.notification.error.historyQuotaReachedMessage": "请求历史记录已满。请清除控制台历史记录或禁止保存新的请求。", - "console.notification.error.noRequestSelectedTitle": "未选择任何请求。将鼠标置于请求内即可选择。", - "console.notification.error.unknownErrorTitle": "未知请求错误", "console.pageHeading": "控制台", "console.requestInProgressBadgeText": "进行中的请求", "console.requestOptions.autoIndentButtonLabel": "应用行首缩进", @@ -7043,9 +7037,6 @@ "sharedUXPackages.fileUpload.uploadCompleteButtonLabel": "上传完成", "sharedUXPackages.fileUpload.uploadDoneToolTipContent": "您的文件已成功上传!", "sharedUXPackages.fileUpload.uploadingButtonLabel": "正在上传", - "sharedUXPackages.no_data_views.esqlButtonLabel": "语言:ES|QL", - "sharedUXPackages.no_data_views.esqlDocsLink": "了解详情。", - "sharedUXPackages.no_data_views.esqlMessage": "或者,您可以直接使用 ES|QL 查询数据。{docsLink}", "sharedUXPackages.noDataConfig.addIntegrationsDescription": "使用 Elastic 代理收集数据并增建分析解决方案。", "sharedUXPackages.noDataConfig.addIntegrationsTitle": "添加集成", "sharedUXPackages.noDataConfig.analytics": "分析", @@ -7067,8 +7058,6 @@ "sharedUXPackages.noDataViewsPrompt.dataViewExplanation": "数据视图标识您要浏览的 Elasticsearch 数据。您可以将数据视图指向一个或多个数据流、索引和索引别名(例如昨天的日志数据),或包含日志数据的所有索引。", "sharedUXPackages.noDataViewsPrompt.learnMore": "希望了解详情?", "sharedUXPackages.noDataViewsPrompt.noPermission.dataViewExplanation": "数据视图标识您要浏览的 Elasticsearch 数据。要创建数据视图,请联系管理员获得所需权限。", - "sharedUXPackages.noDataViewsPrompt.noPermission.title": "您需要权限以创建数据视图", - "sharedUXPackages.noDataViewsPrompt.nowCreate": "现在,创建数据视图。", "sharedUXPackages.noDataViewsPrompt.readDocumentation": "阅读文档", "sharedUXPackages.noDataViewsPrompt.youHaveData": "您在 Elasticsearch 中有数据。", "sharedUXPackages.prompt.errors.notFound.body": "抱歉,找不到您要查找的页面。该页面可能已移除、重命名,或可能根本不存在。", @@ -9158,6 +9147,94 @@ "xpack.actions.serverSideErrors.unavailableLicenseInformationErrorMessage": "操作不可用 - 许可信息当前不可用。", "xpack.actions.subActionsFramework.urlValidationError": "验证 URL 时出错:{message}", "xpack.actions.urlAllowedHostsConfigurationError": "目标 {field} 的“{value}”未添加到 Kibana 配置 xpack.actions.allowedHosts", + "xpack.aiAssistant.askAssistantButton.buttonLabel": "询问助手", + "xpack.aiAssistant.askAssistantButton.popoverContent": "使用 Elastic 助手深入了解您的数据", + "xpack.aiAssistant.assistantSetup.title": "欢迎使用 Elastic AI 助手", + "xpack.aiAssistant.chatActionsMenu.euiButtonIcon.menuLabel": "菜单", + "xpack.aiAssistant.chatActionsMenu.euiToolTip.moreActionsLabel": "更多操作", + "xpack.aiAssistant.chatCollapsedItems.hideEvents": "隐藏 {count} 个事件", + "xpack.aiAssistant.chatCollapsedItems.showEvents": "显示 {count} 个事件", + "xpack.aiAssistant.chatCollapsedItems.toggleButtonLabel": "显示/隐藏项目", + "xpack.aiAssistant.chatFlyout.euiButtonIcon.expandConversationListLabel": "展开对话列表", + "xpack.aiAssistant.chatFlyout.euiButtonIcon.newChatLabel": "新聊天", + "xpack.aiAssistant.chatFlyout.euiToolTip.collapseConversationListLabel": "折叠对话列表", + "xpack.aiAssistant.chatFlyout.euiToolTip.expandConversationListLabel": "展开对话列表", + "xpack.aiAssistant.chatFlyout.euiToolTip.newChatLabel": "新聊天", + "xpack.aiAssistant.chatHeader.actions.connector": "连接器", + "xpack.aiAssistant.chatHeader.actions.copyConversation": "复制对话", + "xpack.aiAssistant.chatHeader.actions.knowledgeBase": "管理知识库", + "xpack.aiAssistant.chatHeader.actions.settings": "AI 助手设置", + "xpack.aiAssistant.chatHeader.actions.title": "操作", + "xpack.aiAssistant.chatHeader.editConversationInput": "编辑对话", + "xpack.aiAssistant.chatHeader.euiButtonIcon.navigateToConversationsLabel": "导航到对话", + "xpack.aiAssistant.chatHeader.euiButtonIcon.toggleFlyoutModeLabel": "切换浮出控件模式", + "xpack.aiAssistant.chatHeader.euiToolTip.flyoutModeLabel.dock": "停靠聊天", + "xpack.aiAssistant.chatHeader.euiToolTip.flyoutModeLabel.undock": "取消停靠聊天", + "xpack.aiAssistant.chatHeader.euiToolTip.navigateToConversationsLabel": "导航到对话", + "xpack.aiAssistant.chatPromptEditor.codeEditor.payloadEditorLabel": "payloadEditor", + "xpack.aiAssistant.chatPromptEditor.euiButtonIcon.submitLabel": "提交", + "xpack.aiAssistant.chatTimeline.actions.copyMessage": "复制消息", + "xpack.aiAssistant.chatTimeline.actions.copyMessageSuccessful": "已复制消息", + "xpack.aiAssistant.chatTimeline.actions.editPrompt": "编辑提示", + "xpack.aiAssistant.chatTimeline.actions.inspectPrompt": "检查提示", + "xpack.aiAssistant.chatTimeline.messages.elasticAssistant.label": "Elastic 助手", + "xpack.aiAssistant.chatTimeline.messages.system.label": "系统", + "xpack.aiAssistant.chatTimeline.messages.user.label": "您", + "xpack.aiAssistant.checkingKbAvailability": "正在检查知识库的可用性", + "xpack.aiAssistant.conversationStartTitle": "已开始对话", + "xpack.aiAssistant.couldNotFindConversationContent": "找不到 ID 为 {conversationId} 的对话。请确保该对话存在并且您具有访问权限。", + "xpack.aiAssistant.couldNotFindConversationTitle": "未找到对话", + "xpack.aiAssistant.disclaimer.disclaimerLabel": "通过集成 LLM 提供商来支持此聊天。众所周知,LLM 有时会提供错误信息,好像它是正确的。Elastic 支持配置并连接到 LLM 提供商和知识库,但不对 LLM 响应负责。", + "xpack.aiAssistant.emptyConversationTitle": "新对话", + "xpack.aiAssistant.errorSettingUpKnowledgeBase": "无法设置知识库", + "xpack.aiAssistant.errorUpdatingConversation": "无法更新对话", + "xpack.aiAssistant.executedFunctionFailureEvent": "无法执行函数 {functionName}", + "xpack.aiAssistant.failedToGetStatus": "无法获取模型状态。", + "xpack.aiAssistant.failedToSetupKnowledgeBase": "无法设置知识库。", + "xpack.aiAssistant.flyout.confirmDeleteButtonText": "删除对话", + "xpack.aiAssistant.flyout.confirmDeleteConversationContent": "此操作无法撤消。", + "xpack.aiAssistant.flyout.confirmDeleteConversationTitle": "删除此对话?", + "xpack.aiAssistant.flyout.failedToDeleteConversation": "无法删除对话", + "xpack.aiAssistant.functionListPopover.euiButtonIcon.selectAFunctionLabel": "选择函数", + "xpack.aiAssistant.functionListPopover.euiToolTip.clearFunction": "清除函数", + "xpack.aiAssistant.functionListPopover.euiToolTip.selectAFunctionLabel": "选择函数", + "xpack.aiAssistant.hideExpandConversationButton.hide": "隐藏聊天", + "xpack.aiAssistant.hideExpandConversationButton.show": "显示聊天", + "xpack.aiAssistant.incorrectLicense.body": "您需要企业级许可证才能使用 Elastic AI 助手。", + "xpack.aiAssistant.incorrectLicense.manageLicense": "管理许可证", + "xpack.aiAssistant.incorrectLicense.subscriptionPlansButton": "订阅计划", + "xpack.aiAssistant.incorrectLicense.title": "升级您的许可证", + "xpack.aiAssistant.initialSetupPanel.setupConnector.buttonLabel": "设置 GenAI 连接器", + "xpack.aiAssistant.initialSetupPanel.setupConnector.description2": "通过为您的 AI 提供商设置连接器,开始使用 Elastic AI 助手。此模型需要支持函数调用。使用 OpenAI 或 Azure 时,建议使用 GPT4。", + "xpack.aiAssistant.installingKb": "正在设置知识库", + "xpack.aiAssistant.newChatButton": "新聊天", + "xpack.aiAssistant.poweredByModel": "由 {model} 提供支持", + "xpack.aiAssistant.prompt.functionList.filter": "筛选", + "xpack.aiAssistant.prompt.functionList.functionList": "函数列表", + "xpack.aiAssistant.prompt.placeholder": "向助手发送消息", + "xpack.aiAssistant.promptEditorNaturalLanguage.euiSelectable.selectAnOptionLabel": "选择选项", + "xpack.aiAssistant.settingsPage.goToConnectorsButtonLabel": "管理连接器", + "xpack.aiAssistant.setupKb": "通过设置知识库来改进体验。", + "xpack.aiAssistant.simulatedFunctionCallingCalloutLabel": "模拟函数调用已启用。您可能会面临性能降级。", + "xpack.aiAssistant.suggestedFunctionEvent": "已请求函数 {functionName}", + "xpack.aiAssistant.technicalPreviewBadgeDescription": "需要 GPT4 以在使用函数调用时(例如,执行根本原因分析、数据可视化等时候)获得更加一致的体验。GPT3.5 可作用于某些更简单的工作流(如解释错误),或在 Kibana 中获得不需要频繁使用函数调用的与 ChatGPT 类似的体验。", + "xpack.aiAssistant.userExecutedFunctionEvent": "已执行函数 {functionName}", + "xpack.aiAssistant.userSuggestedFunctionEvent": "已请求函数 {functionName}", + "xpack.aiAssistant.welcomeMessage.div.checkTrainedModelsToLabel": " {retryInstallingLink} 或检查 {trainedModelsLink},确保 {modelName} 已部署并正在运行。", + "xpack.aiAssistant.welcomeMessage.div.settingUpKnowledgeBaseLabel": "正在设置知识库", + "xpack.aiAssistant.welcomeMessage.inspectErrorsButtonEmptyLabel": "检查问题", + "xpack.aiAssistant.welcomeMessage.issuesDescriptionListTitleLabel": "问题", + "xpack.aiAssistant.welcomeMessage.knowledgeBaseSuccessfullyInstalledLabel": "已成功安装知识库", + "xpack.aiAssistant.welcomeMessage.modelIsNotDeployedLabel": "未部署模型 {modelName}", + "xpack.aiAssistant.welcomeMessage.modelIsNotFullyAllocatedLabel": "{modelName} 的分配状态为 {allocationState}", + "xpack.aiAssistant.welcomeMessage.modelIsNotStartedLabel": "{modelName} 的部署状态为 {deploymentState}", + "xpack.aiAssistant.welcomeMessage.retryButtonLabel": "安装知识库", + "xpack.aiAssistant.welcomeMessage.trainedModelsLinkLabel": "已训练模型", + "xpack.aiAssistant.welcomeMessage.weAreSettingUpTextLabel": "我们正在设置您的知识库。这可能需要若干分钟。此进程处于运行状态时,您可以继续使用该助手。", + "xpack.aiAssistant.welcomeMessageConnectors.connectorsErrorTextLabel": "无法加载连接器", + "xpack.aiAssistant.welcomeMessageConnectors.connectorsForbiddenTextLabel": "缺少获取连接器所需的权限", + "xpack.aiAssistant.welcomeMessageKnowledgeBase.yourKnowledgeBaseIsNotSetUpCorrectlyLabel": "尚未设置您的知识库。", + "xpack.aiAssistant.welcomeMessageKnowledgeBaseSetupErrorPanel.retryInstallingLinkLabel": "重试安装", "xpack.aiops.actions.openChangePointInMlAppName": "在 Aiops 实验室中打开", "xpack.aiops.analysis.columnSelectorAriaLabel": "筛选列", "xpack.aiops.analysis.columnSelectorNotEnoughColumnsSelected": "必须至少选择一列。", @@ -22994,7 +23071,6 @@ "xpack.infra.logs.highlights.highlightTermsFieldLabel": "要突出显示的词", "xpack.infra.logs.index.anomaliesTabTitle": "异常", "xpack.infra.logs.index.logCategoriesBetaBadgeTitle": "类别", - "xpack.infra.logs.index.logsLabel": "日志", "xpack.infra.logs.index.settingsTabTitle": "设置", "xpack.infra.logs.index.streamTabTitle": "流式传输", "xpack.infra.logs.logCategoriesTitle": "类别", @@ -28428,7 +28504,6 @@ "xpack.ml.jobsList.jobDetails.forecastsTable.processingTimeLabel": "处理时间", "xpack.ml.jobsList.jobDetails.forecastsTable.statusLabel": "状态", "xpack.ml.jobsList.jobDetails.forecastsTable.toLabel": "至", - "xpack.ml.jobsList.jobDetails.forecastsTable.viewAriaLabel": "查看在 {createdDate} 创建的预测", "xpack.ml.jobsList.jobDetails.forecastsTable.viewLabel": "查看", "xpack.ml.jobsList.jobDetails.generalTitle": "常规", "xpack.ml.jobsList.jobDetails.influencersTitle": "影响因素", @@ -29858,7 +29933,6 @@ "xpack.ml.trainedModels.modelsList.builtInModelLabel": "内置", "xpack.ml.trainedModels.modelsList.builtInModelMessage": "内置模型", "xpack.ml.trainedModels.modelsList.collapseRow": "折叠", - "xpack.ml.trainedModels.modelsList.createdAtHeader": "创建于", "xpack.ml.trainedModels.modelsList.deleteDisabledWithDeploymentsTooltip": "模型已开始部署", "xpack.ml.trainedModels.modelsList.deleteDisabledWithInferenceServicesTooltip": "模型由 _inference API 使用", "xpack.ml.trainedModels.modelsList.deleteModal.approvePipelinesDeletionLabel": "删除{pipelinesCount, plural, other {管道}}", @@ -29873,9 +29947,7 @@ "xpack.ml.trainedModels.modelsList.deleteModelsButtonLabel": "删除", "xpack.ml.trainedModels.modelsList.deployModelActionLabel": "部署模型", "xpack.ml.trainedModels.modelsList.disableSelectableMessage": "模型有关联的管道", - "xpack.ml.trainedModels.modelsList.downloadCompleteSuccess": "“{modelIds}”{modelIdsLength, plural, other {已}}成功下载。", "xpack.ml.trainedModels.modelsList.downloadFailed": "无法下载“{modelId}”", - "xpack.ml.trainedModels.modelsList.downloadStatusCheckErrorMessage": "无法检查下载状态", "xpack.ml.trainedModels.modelsList.e5Title": "E5 (EmbEddings from bidirEctional Encoder rEpresentations)", "xpack.ml.trainedModels.modelsList.e5v1Description": "E5 (EmbEddings from bidirEctional Encoder rEpresentations)", "xpack.ml.trainedModels.modelsList.e5v1x86Description": "针对 linux-x86_64 进行了优化的 E5 (EmbEddings from bidirEctional Encoder rEpresentations)", @@ -29913,11 +29985,9 @@ "xpack.ml.trainedModels.modelsList.forceStopDialog.selectDeploymentsLegend": "选择要停止的部署", "xpack.ml.trainedModels.modelsList.forceStopDialog.title": "停止{deploymentCount, plural, other {部署}}模型 {modelId}?", "xpack.ml.trainedModels.modelsList.mitLicenseLabel": "许可证:MIT", - "xpack.ml.trainedModels.modelsList.modelDescriptionHeader": "描述", "xpack.ml.trainedModels.modelsList.modelIdHeader": "ID", "xpack.ml.trainedModels.modelsList.modelState.downloadedName": "准备部署", "xpack.ml.trainedModels.modelsList.modelState.downloadingName": "正在下载......", - "xpack.ml.trainedModels.modelsList.modelState.notDownloadedName": "未下载", "xpack.ml.trainedModels.modelsList.modelState.startedName": "已部署", "xpack.ml.trainedModels.modelsList.modelState.startingName": "开始部署......", "xpack.ml.trainedModels.modelsList.modelState.stoppingName": "停止部署......", @@ -29944,14 +30014,10 @@ "xpack.ml.trainedModels.modelsList.startDeployment.updateButton": "更新", "xpack.ml.trainedModels.modelsList.startDeployment.viewElserDocLink": "查看文档", "xpack.ml.trainedModels.modelsList.startFailed": "无法启动“{modelId}”", - "xpack.ml.trainedModels.modelsList.startSuccess": "已成功启动“{modelId}”的部署。", "xpack.ml.trainedModels.modelsList.stateHeader": "状态", "xpack.ml.trainedModels.modelsList.stopDeploymentWarning": "无法停止“{deploymentId}”", "xpack.ml.trainedModels.modelsList.stopFailed": "无法停止“{modelId}”", - "xpack.ml.trainedModels.modelsList.stopSuccess": "已成功停止“{modelId}”的{numberOfDeployments, plural, other {部署}}。", - "xpack.ml.trainedModels.modelsList.successfullyDeletedMessage": "{modelsCount, plural, one {模型 {modelIds}} other {# 个模型}}{modelsCount, plural, other {已}}成功删除", "xpack.ml.trainedModels.modelsList.totalAmountLabel": "已训练的模型总数", - "xpack.ml.trainedModels.modelsList.typeHeader": "类型", "xpack.ml.trainedModels.modelsList.updateDeployment.modalTitle": "更新 {modelId} 部署", "xpack.ml.trainedModels.modelsList.updateFailed": "无法更新“{modelId}”", "xpack.ml.trainedModels.modelsList.updateSuccess": "已成功更新“{modelId}”的部署。", @@ -32353,92 +32419,27 @@ "xpack.observabilityAiAssistant.app.starterPrompts.whatAreSlos.prompt": "什么是 SLO?", "xpack.observabilityAiAssistant.app.starterPrompts.whatAreSlos.title": "SLO", "xpack.observabilityAiAssistant.appTitle": "Observability AI 助手", - "xpack.observabilityAiAssistant.askAssistantButton.buttonLabel": "询问助手", - "xpack.observabilityAiAssistant.askAssistantButton.popoverContent": "使用 Elastic 助手深入了解您的数据", - "xpack.observabilityAiAssistant.askAssistantButton.popoverTitle": "适用于 Observability 的 AI 助手", - "xpack.observabilityAiAssistant.assistantSetup.title": "欢迎使用 Elastic AI 助手", "xpack.observabilityAiAssistant.changesList.dotImpactHigh": "高", "xpack.observabilityAiAssistant.changesList.dotImpactLow": "低", "xpack.observabilityAiAssistant.changesList.dotImpactMedium": "中", "xpack.observabilityAiAssistant.changesList.labelColumnTitle": "标签", "xpack.observabilityAiAssistant.changesList.noChangesDetected": "未检测到更改", "xpack.observabilityAiAssistant.changesList.trendColumnTitle": "趋势", - "xpack.observabilityAiAssistant.chatActionsMenu.euiButtonIcon.menuLabel": "菜单", - "xpack.observabilityAiAssistant.chatActionsMenu.euiToolTip.moreActionsLabel": "更多操作", - "xpack.observabilityAiAssistant.chatCollapsedItems.hideEvents": "隐藏 {count} 个事件", - "xpack.observabilityAiAssistant.chatCollapsedItems.showEvents": "显示 {count} 个事件", - "xpack.observabilityAiAssistant.chatCollapsedItems.toggleButtonLabel": "显示/隐藏项目", "xpack.observabilityAiAssistant.chatCompletionError.conversationNotFoundError": "未找到对话", "xpack.observabilityAiAssistant.chatCompletionError.internalServerError": "发生内部服务器错误", "xpack.observabilityAiAssistant.chatCompletionError.tokenLimitReachedError": "达到了词元限制。词元限制为 {tokenLimit},但当前对话具有 {tokenCount} 个词元。", - "xpack.observabilityAiAssistant.chatFlyout.euiButtonIcon.expandConversationListLabel": "展开对话列表", - "xpack.observabilityAiAssistant.chatFlyout.euiButtonIcon.newChatLabel": "新聊天", - "xpack.observabilityAiAssistant.chatFlyout.euiFlyoutResizable.aiAssistantForObservabilityLabel": "适用于 Observability 聊天浮出控件的 AI 助手", - "xpack.observabilityAiAssistant.chatFlyout.euiToolTip.collapseConversationListLabel": "折叠对话列表", - "xpack.observabilityAiAssistant.chatFlyout.euiToolTip.expandConversationListLabel": "展开对话列表", - "xpack.observabilityAiAssistant.chatFlyout.euiToolTip.newChatLabel": "新聊天", - "xpack.observabilityAiAssistant.chatHeader.actions.connector": "连接器", - "xpack.observabilityAiAssistant.chatHeader.actions.copyConversation": "复制对话", - "xpack.observabilityAiAssistant.chatHeader.actions.knowledgeBase": "管理知识库", - "xpack.observabilityAiAssistant.chatHeader.actions.settings": "AI 助手设置", - "xpack.observabilityAiAssistant.chatHeader.actions.title": "操作", - "xpack.observabilityAiAssistant.chatHeader.editConversationInput": "编辑对话", - "xpack.observabilityAiAssistant.chatHeader.euiButtonIcon.navigateToConversationsLabel": "导航到对话", - "xpack.observabilityAiAssistant.chatHeader.euiButtonIcon.toggleFlyoutModeLabel": "切换浮出控件模式", - "xpack.observabilityAiAssistant.chatHeader.euiToolTip.flyoutModeLabel.dock": "停靠聊天", - "xpack.observabilityAiAssistant.chatHeader.euiToolTip.flyoutModeLabel.undock": "取消停靠聊天", - "xpack.observabilityAiAssistant.chatHeader.euiToolTip.navigateToConversationsLabel": "导航到对话", - "xpack.observabilityAiAssistant.chatPromptEditor.codeEditor.payloadEditorLabel": "payloadEditor", - "xpack.observabilityAiAssistant.chatPromptEditor.euiButtonIcon.submitLabel": "提交", "xpack.observabilityAiAssistant.chatService.div.helloLabel": "您好", - "xpack.observabilityAiAssistant.chatTimeline.actions.copyMessage": "复制消息", - "xpack.observabilityAiAssistant.chatTimeline.actions.copyMessageSuccessful": "已复制消息", - "xpack.observabilityAiAssistant.chatTimeline.actions.editPrompt": "编辑提示", - "xpack.observabilityAiAssistant.chatTimeline.actions.inspectPrompt": "检查提示", - "xpack.observabilityAiAssistant.chatTimeline.messages.elasticAssistant.label": "Elastic 助手", - "xpack.observabilityAiAssistant.chatTimeline.messages.system.label": "系统", - "xpack.observabilityAiAssistant.chatTimeline.messages.user.label": "您", - "xpack.observabilityAiAssistant.checkingKbAvailability": "正在检查知识库的可用性", "xpack.observabilityAiAssistant.connectorSelector.connectorSelectLabel": "连接器:", "xpack.observabilityAiAssistant.connectorSelector.empty": "无连接器", "xpack.observabilityAiAssistant.connectorSelector.error": "无法加载连接器", - "xpack.observabilityAiAssistant.conversationList.deleteConversationIconLabel": "删除", - "xpack.observabilityAiAssistant.conversationList.errorMessage": "无法加载", - "xpack.observabilityAiAssistant.conversationList.noConversations": "无对话", - "xpack.observabilityAiAssistant.conversationList.title": "以前", "xpack.observabilityAiAssistant.conversationsDeepLinkTitle": "对话", - "xpack.observabilityAiAssistant.conversationStartTitle": "已开始对话", - "xpack.observabilityAiAssistant.couldNotFindConversationContent": "找不到 ID 为 {conversationId} 的对话。请确保该对话存在并且您具有访问权限。", - "xpack.observabilityAiAssistant.couldNotFindConversationTitle": "未找到对话", - "xpack.observabilityAiAssistant.disclaimer.disclaimerLabel": "通过集成 LLM 提供商来支持此聊天。众所周知,LLM 有时会提供错误信息,好像它是正确的。Elastic 支持配置并连接到 LLM 提供商和知识库,但不对 LLM 响应负责。", - "xpack.observabilityAiAssistant.emptyConversationTitle": "新对话", - "xpack.observabilityAiAssistant.errorSettingUpKnowledgeBase": "无法设置知识库", - "xpack.observabilityAiAssistant.errorUpdatingConversation": "无法更新对话", - "xpack.observabilityAiAssistant.executedFunctionFailureEvent": "无法执行函数 {functionName}", "xpack.observabilityAiAssistant.experimentalTitle": "技术预览", "xpack.observabilityAiAssistant.experimentalTooltip": "此功能处于技术预览状态,在未来版本中可能会更改或完全移除。Elastic 将努力修复任何问题,但处于技术预览状态的功能不受正式 GA 功能支持 SLA 的约束。", "xpack.observabilityAiAssistant.failedLoadingResponseText": "无法加载响应", - "xpack.observabilityAiAssistant.failedToGetStatus": "无法获取模型状态。", "xpack.observabilityAiAssistant.failedToLoadResponse": "无法加载来自 AI 助手的响应", - "xpack.observabilityAiAssistant.failedToSetupKnowledgeBase": "无法设置知识库。", "xpack.observabilityAiAssistant.featureRegistry.featureName": "Observability AI 助手", "xpack.observabilityAiAssistant.feedbackButtons.em.thanksForYourFeedbackLabel": "感谢您提供反馈", - "xpack.observabilityAiAssistant.flyout.confirmDeleteButtonText": "删除对话", - "xpack.observabilityAiAssistant.flyout.confirmDeleteConversationContent": "此操作无法撤消。", - "xpack.observabilityAiAssistant.flyout.confirmDeleteConversationTitle": "删除此对话?", - "xpack.observabilityAiAssistant.flyout.failedToDeleteConversation": "无法删除对话", "xpack.observabilityAiAssistant.functionCallLimitExceeded": "\n\n注意:即使超出了限制,助手仍尝试调用了函数", - "xpack.observabilityAiAssistant.functionListPopover.euiButtonIcon.selectAFunctionLabel": "选择函数", - "xpack.observabilityAiAssistant.functionListPopover.euiToolTip.clearFunction": "清除函数", - "xpack.observabilityAiAssistant.functionListPopover.euiToolTip.selectAFunctionLabel": "选择函数", - "xpack.observabilityAiAssistant.hideExpandConversationButton.hide": "隐藏聊天", - "xpack.observabilityAiAssistant.hideExpandConversationButton.show": "显示聊天", - "xpack.observabilityAiAssistant.incorrectLicense.body": "您需要企业级许可证才能使用 Elastic AI 助手。", - "xpack.observabilityAiAssistant.incorrectLicense.manageLicense": "管理许可证", - "xpack.observabilityAiAssistant.incorrectLicense.subscriptionPlansButton": "订阅计划", - "xpack.observabilityAiAssistant.incorrectLicense.title": "升级您的许可证", - "xpack.observabilityAiAssistant.initialSetupPanel.setupConnector.buttonLabel": "设置 GenAI 连接器", - "xpack.observabilityAiAssistant.initialSetupPanel.setupConnector.description2": "通过为您的 AI 提供商设置连接器,开始使用 Elastic AI 助手。此模型需要支持函数调用。使用 OpenAI 或 Azure 时,建议使用 GPT4。", "xpack.observabilityAiAssistant.insight.actions": "操作", "xpack.observabilityAiAssistant.insight.actions.connector": "连接器", "xpack.observabilityAiAssistant.insight.actions.editPrompt": "编辑提示", @@ -32456,7 +32457,6 @@ "xpack.observabilityAiAssistant.insight.response.startChat": "开始聊天", "xpack.observabilityAiAssistant.insight.sendPromptEdit": "发送提示", "xpack.observabilityAiAssistant.insightModifiedPrompt": "此洞察已被修改。", - "xpack.observabilityAiAssistant.installingKb": "正在设置知识库", "xpack.observabilityAiAssistant.lensESQLFunction.displayChart": "显示图表", "xpack.observabilityAiAssistant.lensESQLFunction.displayTable": "显示表", "xpack.observabilityAiAssistant.lensESQLFunction.edit": "编辑可视化", @@ -32471,42 +32471,14 @@ "xpack.observabilityAiAssistant.missingCredentialsCallout.title": "凭据缺失", "xpack.observabilityAiAssistant.navControl.initFailureErrorTitle": "无法初始化 Observability AI 助手", "xpack.observabilityAiAssistant.navControl.openTheAIAssistantPopoverLabel": "打开 AI 助手", - "xpack.observabilityAiAssistant.newChatButton": "新聊天", - "xpack.observabilityAiAssistant.poweredByModel": "由 {model} 提供支持", - "xpack.observabilityAiAssistant.prompt.functionList.filter": "筛选", - "xpack.observabilityAiAssistant.prompt.functionList.functionList": "函数列表", - "xpack.observabilityAiAssistant.prompt.placeholder": "向助手发送消息", - "xpack.observabilityAiAssistant.promptEditorNaturalLanguage.euiSelectable.selectAnOptionLabel": "选择选项", "xpack.observabilityAiAssistant.regenerateResponseButtonLabel": "重新生成", "xpack.observabilityAiAssistant.requiredConnectorField": "“连接器”必填。", "xpack.observabilityAiAssistant.requiredMessageTextField": "“消息”必填。", "xpack.observabilityAiAssistant.resetDefaultPrompt": "重置为默认值", "xpack.observabilityAiAssistant.runThisQuery": "显示结果", - "xpack.observabilityAiAssistant.settingsPage.goToConnectorsButtonLabel": "管理连接器", - "xpack.observabilityAiAssistant.setupKb": "通过设置知识库来改进体验。", - "xpack.observabilityAiAssistant.simulatedFunctionCallingCalloutLabel": "模拟函数调用已启用。您可能会面临性能降级。", "xpack.observabilityAiAssistant.stopGeneratingButtonLabel": "停止生成", - "xpack.observabilityAiAssistant.suggestedFunctionEvent": "已请求函数 {functionName}", - "xpack.observabilityAiAssistant.technicalPreviewBadgeDescription": "需要 GPT4 以在使用函数调用时(例如,执行根本原因分析、数据可视化等时候)获得更加一致的体验。GPT3.5 可作用于某些更简单的工作流(如解释错误),或在 Kibana 中获得不需要频繁使用函数调用的与 ChatGPT 类似的体验。", "xpack.observabilityAiAssistant.tokenLimitError": "此对话已超出词元限制。最大词元限制为 **{tokenLimit}**,但当前对话具有 **{tokenCount}** 个词元。请启动新对话以继续。", - "xpack.observabilityAiAssistant.userExecutedFunctionEvent": "已执行函数 {functionName}", - "xpack.observabilityAiAssistant.userSuggestedFunctionEvent": "已请求函数 {functionName}", "xpack.observabilityAiAssistant.visualizeThisQuery": "可视化此查询", - "xpack.observabilityAiAssistant.welcomeMessage.div.checkTrainedModelsToLabel": " {retryInstallingLink} 或检查 {trainedModelsLink},确保 {modelName} 已部署并正在运行。", - "xpack.observabilityAiAssistant.welcomeMessage.div.settingUpKnowledgeBaseLabel": "正在设置知识库", - "xpack.observabilityAiAssistant.welcomeMessage.inspectErrorsButtonEmptyLabel": "检查问题", - "xpack.observabilityAiAssistant.welcomeMessage.issuesDescriptionListTitleLabel": "问题", - "xpack.observabilityAiAssistant.welcomeMessage.knowledgeBaseSuccessfullyInstalledLabel": "已成功安装知识库", - "xpack.observabilityAiAssistant.welcomeMessage.modelIsNotDeployedLabel": "未部署模型 {modelName}", - "xpack.observabilityAiAssistant.welcomeMessage.modelIsNotFullyAllocatedLabel": "{modelName} 的分配状态为 {allocationState}", - "xpack.observabilityAiAssistant.welcomeMessage.modelIsNotStartedLabel": "{modelName} 的部署状态为 {deploymentState}", - "xpack.observabilityAiAssistant.welcomeMessage.retryButtonLabel": "安装知识库", - "xpack.observabilityAiAssistant.welcomeMessage.trainedModelsLinkLabel": "已训练模型", - "xpack.observabilityAiAssistant.welcomeMessage.weAreSettingUpTextLabel": "我们正在设置您的知识库。这可能需要若干分钟。此进程处于运行状态时,您可以继续使用该助手。", - "xpack.observabilityAiAssistant.welcomeMessageConnectors.connectorsErrorTextLabel": "无法加载连接器", - "xpack.observabilityAiAssistant.welcomeMessageConnectors.connectorsForbiddenTextLabel": "缺少获取连接器所需的权限", - "xpack.observabilityAiAssistant.welcomeMessageKnowledgeBase.yourKnowledgeBaseIsNotSetUpCorrectlyLabel": "尚未设置您的知识库。", - "xpack.observabilityAiAssistant.welcomeMessageKnowledgeBaseSetupErrorPanel.retryInstallingLinkLabel": "重试安装", "xpack.observabilityAiAssistantManagement.apmSettings.save.error": "保存设置时出错", "xpack.observabilityAiAssistantManagement.app.description": "管理适用于 Observability 的 AI 助手。", "xpack.observabilityAiAssistantManagement.app.title": "适用于 Observability 的 AI 助手", @@ -35939,8 +35911,6 @@ "xpack.securitySolution.detectionEngine.createRule.stepDefineRule.customThreatQueryFieldRequiredEmptyError": "所有匹配项都需要字段和威胁索引字段。", "xpack.securitySolution.detectionEngine.createRule.stepDefineRule.customThreatQueryFieldRequiredError": "至少需要一个指标匹配。", "xpack.securitySolution.detectionEngine.createRule.stepDefineRule.dataViewSelectorFieldRequired": "请选择可用的数据视图或索引模式。", - "xpack.securitySolution.detectionEngine.createRule.stepDefineRule.enableThresholdSuppressionForFieldsLabel": "选定字段阻止告警:{fieldsString}(技术预览)", - "xpack.securitySolution.detectionEngine.createRule.stepDefineRule.enableThresholdSuppressionLabel": "阻止告警(技术预览)", "xpack.securitySolution.detectionEngine.createRule.stepDefineRule.EqlQueryBarLabel": "EQL 查询", "xpack.securitySolution.detectionEngine.createRule.stepDefineRule.eqlQueryFieldRequiredError": "EQL 查询必填。", "xpack.securitySolution.detectionEngine.createRule.stepDefineRule.eqlSequenceSuppressionDisableText": "EQL 序列查询不支持阻止。", @@ -39424,18 +39394,13 @@ "xpack.securitySolution.notes.management.createdByColumnTitle": "创建者", "xpack.securitySolution.notes.management.createdColumnTitle": "创建时间", "xpack.securitySolution.notes.management.deleteAction": "删除", - "xpack.securitySolution.notes.management.deleteDescription": "删除此备注", "xpack.securitySolution.notes.management.deleteNotesCancel": "取消", "xpack.securitySolution.notes.management.deleteNotesConfirm": "是否确定要删除 {selectedNotes} 个{selectedNotes, plural, other {备注}}?", - "xpack.securitySolution.notes.management.deleteNotesModalTitle": "删除备注?", "xpack.securitySolution.notes.management.deleteSelected": "删除所选备注", - "xpack.securitySolution.notes.management.eventIdColumnTitle": "查看文档", "xpack.securitySolution.notes.management.noteContentColumnTitle": "备注内容", "xpack.securitySolution.notes.management.openTimeline": "打开时间线", "xpack.securitySolution.notes.management.refresh": "刷新", "xpack.securitySolution.notes.management.tableError": "无法加载备注", - "xpack.securitySolution.notes.management.timelineColumnTitle": "时间线", - "xpack.securitySolution.notes.management.viewEventInTimeline": "在时间线中查看事件", "xpack.securitySolution.notes.noteLabel": "备注", "xpack.securitySolution.notes.notesTitle": "备注", "xpack.securitySolution.notes.search.FilterByUserOrNotePlaceholder": "按用户或备注筛选", diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/rules_setting/flapping/rules_settings_flapping_form_section.tsx b/x-pack/plugins/triggers_actions_ui/public/application/components/rules_setting/flapping/rules_settings_flapping_form_section.tsx index a78658044a192..1b38eede40e68 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/components/rules_setting/flapping/rules_settings_flapping_form_section.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/rules_setting/flapping/rules_settings_flapping_form_section.tsx @@ -82,6 +82,7 @@ export const RulesSettingsFlappingFormSection = memo( diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/rules_setting/rules_settings_link.test.tsx b/x-pack/plugins/triggers_actions_ui/public/application/components/rules_setting/rules_settings_link.test.tsx index 8d32eb2c9940c..e1cdf5a8ee150 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/components/rules_setting/rules_settings_link.test.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/rules_setting/rules_settings_link.test.tsx @@ -14,12 +14,12 @@ import { coreMock } from '@kbn/core/public/mocks'; import { RulesSettingsFlapping, RulesSettingsQueryDelay } from '@kbn/alerting-plugin/common'; import { RulesSettingsLink } from './rules_settings_link'; import { useKibana } from '../../../common/lib/kibana'; -import { getFlappingSettings } from '../../lib/rule_api/get_flapping_settings'; +import { fetchFlappingSettings } from '@kbn/alerts-ui-shared/src/common/apis/fetch_flapping_settings'; import { getQueryDelaySettings } from '../../lib/rule_api/get_query_delay_settings'; jest.mock('../../../common/lib/kibana'); -jest.mock('../../lib/rule_api/get_flapping_settings', () => ({ - getFlappingSettings: jest.fn(), +jest.mock('@kbn/alerts-ui-shared/src/common/apis/fetch_flapping_settings', () => ({ + fetchFlappingSettings: jest.fn(), })); jest.mock('../../lib/rule_api/get_query_delay_settings', () => ({ getQueryDelaySettings: jest.fn(), @@ -38,8 +38,8 @@ const useKibanaMock = useKibana as jest.Mocked; const mocks = coreMock.createSetup(); -const getFlappingSettingsMock = getFlappingSettings as unknown as jest.MockedFunction< - typeof getFlappingSettings +const fetchFlappingSettingsMock = fetchFlappingSettings as unknown as jest.MockedFunction< + typeof fetchFlappingSettings >; const getQueryDelaySettingsMock = getQueryDelaySettings as unknown as jest.MockedFunction< typeof getQueryDelaySettings @@ -88,7 +88,7 @@ describe('rules_settings_link', () => { readQueryDelaySettingsUI: true, }, }; - getFlappingSettingsMock.mockResolvedValue(mockFlappingSetting); + fetchFlappingSettingsMock.mockResolvedValue(mockFlappingSetting); getQueryDelaySettingsMock.mockResolvedValue(mockQueryDelaySetting); }); diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/rules_setting/rules_settings_modal.test.tsx b/x-pack/plugins/triggers_actions_ui/public/application/components/rules_setting/rules_settings_modal.test.tsx index 592705b56984d..1dea8bdf88a6e 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/components/rules_setting/rules_settings_modal.test.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/rules_setting/rules_settings_modal.test.tsx @@ -15,14 +15,14 @@ import { IToasts } from '@kbn/core/public'; import { RulesSettingsFlapping, RulesSettingsQueryDelay } from '@kbn/alerting-plugin/common'; import { RulesSettingsModal, RulesSettingsModalProps } from './rules_settings_modal'; import { useKibana } from '../../../common/lib/kibana'; -import { getFlappingSettings } from '../../lib/rule_api/get_flapping_settings'; +import { fetchFlappingSettings } from '@kbn/alerts-ui-shared/src/common/apis/fetch_flapping_settings'; import { updateFlappingSettings } from '../../lib/rule_api/update_flapping_settings'; import { getQueryDelaySettings } from '../../lib/rule_api/get_query_delay_settings'; import { updateQueryDelaySettings } from '../../lib/rule_api/update_query_delay_settings'; jest.mock('../../../common/lib/kibana'); -jest.mock('../../lib/rule_api/get_flapping_settings', () => ({ - getFlappingSettings: jest.fn(), +jest.mock('@kbn/alerts-ui-shared/src/common/apis/fetch_flapping_settings', () => ({ + fetchFlappingSettings: jest.fn(), })); jest.mock('../../lib/rule_api/update_flapping_settings', () => ({ updateFlappingSettings: jest.fn(), @@ -47,8 +47,8 @@ const useKibanaMock = useKibana as jest.Mocked; const mocks = coreMock.createSetup(); -const getFlappingSettingsMock = getFlappingSettings as unknown as jest.MockedFunction< - typeof getFlappingSettings +const fetchFlappingSettingsMock = fetchFlappingSettings as unknown as jest.MockedFunction< + typeof fetchFlappingSettings >; const updateFlappingSettingsMock = updateFlappingSettings as unknown as jest.MockedFunction< typeof updateFlappingSettings @@ -142,7 +142,7 @@ describe('rules_settings_modal', () => { useKibanaMock().services.isServerless = true; - getFlappingSettingsMock.mockResolvedValue(mockFlappingSetting); + fetchFlappingSettingsMock.mockResolvedValue(mockFlappingSetting); updateFlappingSettingsMock.mockResolvedValue(mockFlappingSetting); getQueryDelaySettingsMock.mockResolvedValue(mockQueryDelaySetting); updateQueryDelaySettingsMock.mockResolvedValue(mockQueryDelaySetting); @@ -156,7 +156,7 @@ describe('rules_settings_modal', () => { test('renders flapping settings correctly', async () => { const result = render(); - expect(getFlappingSettingsMock).toHaveBeenCalledTimes(1); + expect(fetchFlappingSettingsMock).toHaveBeenCalledTimes(1); await waitForModalLoad(); expect( result.getByTestId('rulesSettingsFlappingEnableSwitch').getAttribute('aria-checked') @@ -204,7 +204,7 @@ describe('rules_settings_modal', () => { test('reset flapping settings to initial state on cancel without triggering another server reload', async () => { const result = render(); - expect(getFlappingSettingsMock).toHaveBeenCalledTimes(1); + expect(fetchFlappingSettingsMock).toHaveBeenCalledTimes(1); expect(getQueryDelaySettingsMock).toHaveBeenCalledTimes(1); await waitForModalLoad(); @@ -228,7 +228,7 @@ describe('rules_settings_modal', () => { expect(lookBackWindowInput.getAttribute('value')).toBe('10'); expect(statusChangeThresholdInput.getAttribute('value')).toBe('10'); - expect(getFlappingSettingsMock).toHaveBeenCalledTimes(1); + expect(fetchFlappingSettingsMock).toHaveBeenCalledTimes(1); expect(getQueryDelaySettingsMock).toHaveBeenCalledTimes(1); }); diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/rules_setting/rules_settings_modal.tsx b/x-pack/plugins/triggers_actions_ui/public/application/components/rules_setting/rules_settings_modal.tsx index 4431f05975906..09828e067369b 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/components/rules_setting/rules_settings_modal.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/rules_setting/rules_settings_modal.tsx @@ -26,8 +26,8 @@ import { EuiSpacer, EuiEmptyPrompt, } from '@elastic/eui'; +import { useFetchFlappingSettings } from '@kbn/alerts-ui-shared/src/common/hooks/use_fetch_flapping_settings'; import { useKibana } from '../../../common/lib/kibana'; -import { useGetFlappingSettings } from '../../hooks/use_get_flapping_settings'; import { RulesSettingsFlappingSection } from './flapping/rules_settings_flapping_section'; import { RulesSettingsQueryDelaySection } from './query_delay/rules_settings_query_delay_section'; import { useGetQueryDelaySettings } from '../../hooks/use_get_query_delay_settings'; @@ -93,6 +93,7 @@ export const RulesSettingsModal = memo((props: RulesSettingsModalProps) => { const { application: { capabilities }, isServerless, + http, } = useKibana().services; const { rulesSettings: { @@ -109,7 +110,8 @@ export const RulesSettingsModal = memo((props: RulesSettingsModalProps) => { const [queryDelaySettings, hasQueryDelayChanged, setQueryDelaySettings, resetQueryDelaySettings] = useResettableState(); - const { isLoading: isFlappingLoading, isError: hasFlappingError } = useGetFlappingSettings({ + const { isLoading: isFlappingLoading, isError: hasFlappingError } = useFetchFlappingSettings({ + http, enabled: isVisible, onSuccess: (fetchedSettings) => { if (!flappingSettings) { diff --git a/x-pack/plugins/triggers_actions_ui/public/application/hooks/use_get_flapping_settings.ts b/x-pack/plugins/triggers_actions_ui/public/application/hooks/use_get_flapping_settings.ts deleted file mode 100644 index 26b9fdcaeb1c2..0000000000000 --- a/x-pack/plugins/triggers_actions_ui/public/application/hooks/use_get_flapping_settings.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; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { useQuery } from '@tanstack/react-query'; -import { RulesSettingsFlapping } from '@kbn/alerting-plugin/common'; -import { useKibana } from '../../common/lib/kibana'; -import { getFlappingSettings } from '../lib/rule_api/get_flapping_settings'; - -interface UseGetFlappingSettingsProps { - enabled: boolean; - onSuccess?: (settings: RulesSettingsFlapping) => void; -} - -export const useGetFlappingSettings = (props: UseGetFlappingSettingsProps) => { - const { enabled, onSuccess } = props; - const { http } = useKibana().services; - - const queryFn = () => { - return getFlappingSettings({ http }); - }; - - const { data, isFetching, isError, isLoadingError, isLoading, isInitialLoading } = useQuery({ - queryKey: ['getFlappingSettings'], - queryFn, - onSuccess, - enabled, - refetchOnWindowFocus: false, - retry: false, - }); - - return { - isInitialLoading, - isLoading: isLoading || isFetching, - isError: isError || isLoadingError, - data, - }; -}; diff --git a/x-pack/plugins/triggers_actions_ui/public/application/lib/rule_api/get_flapping_settings.ts b/x-pack/plugins/triggers_actions_ui/public/application/lib/rule_api/get_flapping_settings.ts deleted file mode 100644 index 931b1037ef729..0000000000000 --- a/x-pack/plugins/triggers_actions_ui/public/application/lib/rule_api/get_flapping_settings.ts +++ /dev/null @@ -1,28 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { HttpSetup } from '@kbn/core/public'; -import { AsApiContract, RewriteRequestCase } from '@kbn/actions-plugin/common'; -import { RulesSettingsFlapping } from '@kbn/alerting-plugin/common'; -import { INTERNAL_BASE_ALERTING_API_PATH } from '../../constants'; - -const rewriteBodyRes: RewriteRequestCase = ({ - look_back_window: lookBackWindow, - status_change_threshold: statusChangeThreshold, - ...rest -}: any) => ({ - ...rest, - lookBackWindow, - statusChangeThreshold, -}); - -export const getFlappingSettings = async ({ http }: { http: HttpSetup }) => { - const res = await http.get>( - `${INTERNAL_BASE_ALERTING_API_PATH}/rules/settings/_flapping` - ); - return rewriteBodyRes(res); -}; diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/rule_form/rule_add.test.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/rule_form/rule_add.test.tsx index af8bda5704b0f..c7b2876d83d84 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/rule_form/rule_add.test.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/rule_form/rule_add.test.tsx @@ -67,8 +67,8 @@ jest.mock('../../lib/action_connector_api', () => ({ loadAllActions: jest.fn(), })); -jest.mock('../../lib/rule_api/get_flapping_settings', () => ({ - getFlappingSettings: jest.fn().mockResolvedValue({ +jest.mock('@kbn/alerts-ui-shared/src/common/apis/fetch_flapping_settings', () => ({ + fetchFlappingSettings: jest.fn().mockResolvedValue({ lookBackWindow: 20, statusChangeThreshold: 20, }), diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/rule_form/rule_add.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/rule_form/rule_add.tsx index 8657248a29df3..ccdca1bd1250d 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/rule_form/rule_add.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/rule_form/rule_add.tsx @@ -14,6 +14,7 @@ import { toMountPoint } from '@kbn/react-kibana-mount'; import { parseRuleCircuitBreakerErrorMessage } from '@kbn/alerting-plugin/common'; import { createRule, CreateRuleBody } from '@kbn/alerts-ui-shared/src/common/apis/create_rule'; import { fetchUiConfig as triggersActionsUiConfig } from '@kbn/alerts-ui-shared/src/common/apis/fetch_ui_config'; +import { IS_RULE_SPECIFIC_FLAPPING_ENABLED } from '@kbn/alerts-ui-shared/src/common/constants/rule_flapping'; import { Rule, RuleTypeParams, @@ -37,7 +38,6 @@ import { hasShowActionsCapability } from '../../lib/capabilities'; import RuleAddFooter from './rule_add_footer'; import { HealthContextProvider } from '../../context/health_context'; import { useKibana } from '../../../common/lib/kibana'; -import { IS_RULE_SPECIFIC_FLAPPING_ENABLED } from '../../../common/constants'; import { hasRuleChanged, haveRuleParamsChanged } from './has_rule_changed'; import { getRuleWithInvalidatedFields } from '../../lib/value_validators'; import { DEFAULT_RULE_INTERVAL, MULTI_CONSUMER_RULE_TYPE_IDS } from '../../constants'; diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/rule_form/rule_edit.test.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/rule_form/rule_edit.test.tsx index 331b10505a5d7..243236d7f6b93 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/rule_form/rule_edit.test.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/rule_form/rule_edit.test.tsx @@ -63,8 +63,8 @@ jest.mock('@kbn/alerts-ui-shared/src/common/apis/fetch_ui_health_status', () => fetchUiHealthStatus: jest.fn(() => ({ isRulesAvailable: true })), })); -jest.mock('../../lib/rule_api/get_flapping_settings', () => ({ - getFlappingSettings: jest.fn().mockResolvedValue({ +jest.mock('@kbn/alerts-ui-shared/src/common/apis/fetch_flapping_settings', () => ({ + fetchFlappingSettings: jest.fn().mockResolvedValue({ lookBackWindow: 20, statusChangeThreshold: 20, }), diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/rule_form/rule_edit.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/rule_form/rule_edit.tsx index 72eab243ad0c8..a24fd0eec2eb1 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/rule_form/rule_edit.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/rule_form/rule_edit.tsx @@ -30,7 +30,7 @@ import { toMountPoint } from '@kbn/react-kibana-mount'; import { parseRuleCircuitBreakerErrorMessage } from '@kbn/alerting-plugin/common'; import { updateRule } from '@kbn/alerts-ui-shared/src/common/apis/update_rule'; import { fetchUiConfig as triggersActionsUiConfig } from '@kbn/alerts-ui-shared/src/common/apis/fetch_ui_config'; -import { IS_RULE_SPECIFIC_FLAPPING_ENABLED } from '../../../common/constants'; +import { IS_RULE_SPECIFIC_FLAPPING_ENABLED } from '@kbn/alerts-ui-shared/src/common/constants/rule_flapping'; import { Rule, RuleFlyoutCloseReason, diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/rule_form/rule_form.test.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/rule_form/rule_form.test.tsx index 38ee1c73ac40b..17bdcc92997ca 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/rule_form/rule_form.test.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/rule_form/rule_form.test.tsx @@ -71,8 +71,8 @@ jest.mock('../../lib/capabilities', () => ({ hasShowActionsCapability: jest.fn(() => true), hasExecuteActionsCapability: jest.fn(() => true), })); -jest.mock('../../lib/rule_api/get_flapping_settings', () => ({ - getFlappingSettings: jest.fn().mockResolvedValue({ +jest.mock('@kbn/alerts-ui-shared/src/common/apis/fetch_flapping_settings', () => ({ + fetchFlappingSettings: jest.fn().mockResolvedValue({ lookBackWindow: 20, statusChangeThreshold: 20, }), diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/rule_form/rule_form.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/rule_form/rule_form.tsx index c3f79c3458374..665dd93325c2b 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/rule_form/rule_form.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/rule_form/rule_form.tsx @@ -62,9 +62,11 @@ import { isActionGroupDisabledForActionTypeId, RuleActionAlertsFilterProperty, RuleActionKey, + Flapping, } from '@kbn/alerting-plugin/common'; import { AlertingConnectorFeatureId } from '@kbn/actions-plugin/common'; import { AlertConsumers } from '@kbn/rule-data-utils'; +import { IS_RULE_SPECIFIC_FLAPPING_ENABLED } from '@kbn/alerts-ui-shared/src/common/constants/rule_flapping'; import { RuleReducerAction, InitialRule } from './rule_reducer'; import { RuleTypeModel, @@ -91,10 +93,7 @@ import { ruleTypeGroupCompare, ruleTypeUngroupedCompare, } from '../../lib/rule_type_compare'; -import { - IS_RULE_SPECIFIC_FLAPPING_ENABLED, - VIEW_LICENSE_OPTIONS_LINK, -} from '../../../common/constants'; +import { VIEW_LICENSE_OPTIONS_LINK } from '../../../common/constants'; import { MULTI_CONSUMER_RULE_TYPE_IDS } from '../../constants'; import { SectionLoading } from '../../components/section_loading'; import { RuleFormConsumerSelection, VALID_CONSUMERS } from './rule_form_consumer_selection'; @@ -882,7 +881,7 @@ export const RuleForm = ({ alertDelay={alertDelay} flappingSettings={rule.flapping} onAlertDelayChange={onAlertDelayChange} - onFlappingChange={(flapping) => setRuleProperty('flapping', flapping)} + onFlappingChange={(flapping) => setRuleProperty('flapping', flapping as Flapping)} enabledFlapping={IS_RULE_SPECIFIC_FLAPPING_ENABLED} /> diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/rule_form/rule_form_advanced_options.test.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/rule_form/rule_form_advanced_options.test.tsx index f6534f7451405..25c6de0225edb 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/rule_form/rule_form_advanced_options.test.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/rule_form/rule_form_advanced_options.test.tsx @@ -88,7 +88,7 @@ describe('ruleFormAdvancedOptions', () => { expect(screen.getByTestId('ruleFormAdvancedOptionsOverrideSwitch')).not.toBeChecked(); expect(screen.queryByText('Custom')).not.toBeInTheDocument(); expect(screen.getByTestId('ruleSettingsFlappingMessage')).toHaveTextContent( - 'An alert is flapping if it changes status at least 3 times in the last 10 rule runs.' + 'All rules (in this space) detect an alert is flapping when it changes status at least 3 times in the last 10 rule runs.' ); await userEvent.click(screen.getByTestId('ruleFormAdvancedOptionsOverrideSwitch')); @@ -121,7 +121,7 @@ describe('ruleFormAdvancedOptions', () => { expect(screen.getByTestId('lookBackWindowRangeInput')).toHaveValue('6'); expect(screen.getByTestId('statusChangeThresholdRangeInput')).toHaveValue('4'); expect(screen.getByTestId('ruleSettingsFlappingMessage')).toHaveTextContent( - 'An alert is flapping if it changes status at least 4 times in the last 6 rule runs.' + 'This rule detects an alert is flapping if it changes status at least 4 times in the last 6 rule runs.' ); await userEvent.click(screen.getByTestId('ruleFormAdvancedOptionsOverrideSwitch')); @@ -157,6 +157,10 @@ describe('ruleFormAdvancedOptions', () => { expect(screen.queryByText('Custom')).not.toBeInTheDocument(); expect(screen.queryByTestId('ruleFormAdvancedOptionsOverrideSwitch')).not.toBeInTheDocument(); expect(screen.queryByTestId('ruleSettingsFlappingMessage')).not.toBeInTheDocument(); + + await userEvent.click(screen.getByTestId('ruleSettingsFlappingFormTooltipButton')); + + expect(screen.getByTestId('ruleSettingsFlappingFormTooltipContent')).toBeInTheDocument(); }); test('should allow for flapping inputs to be modified', async () => { diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/rule_form/rule_form_advanced_options.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/rule_form/rule_form_advanced_options.tsx index ca6e17451c1aa..00ad6186d58e8 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/rule_form/rule_form_advanced_options.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/rule_form/rule_form_advanced_options.tsx @@ -5,36 +5,21 @@ * 2.0. */ -import React, { useCallback, useMemo, useRef, useState } from 'react'; +import React, { useCallback, useState } from 'react'; import { i18n } from '@kbn/i18n'; import { - EuiBadge, EuiFieldNumber, EuiFlexGroup, EuiFlexItem, EuiFormRow, EuiIconTip, EuiPanel, - EuiSwitch, - EuiText, - useIsWithinMinBreakpoint, - useEuiTheme, - EuiHorizontalRule, - EuiSpacer, - EuiSplitPanel, EuiLoadingSpinner, - EuiLink, - EuiButtonIcon, - EuiPopover, - EuiPopoverTitle, - EuiOutsideClickDetector, } from '@elastic/eui'; -import { RuleSettingsFlappingInputs } from '@kbn/alerts-ui-shared/src/rule_settings/rule_settings_flapping_inputs'; -import { RuleSettingsFlappingMessage } from '@kbn/alerts-ui-shared/src/rule_settings/rule_settings_flapping_message'; -import { Rule } from '@kbn/alerts-ui-shared'; -import { FormattedMessage } from '@kbn/i18n-react'; -import { Flapping } from '@kbn/alerting-plugin/common'; -import { useGetFlappingSettings } from '../../hooks/use_get_flapping_settings'; +import { RuleSpecificFlappingProperties } from '@kbn/alerting-types/rule_settings'; +import { RuleSettingsFlappingForm } from '@kbn/alerts-ui-shared/src/rule_settings/rule_settings_flapping_form'; +import { RuleSettingsFlappingTitleTooltip } from '@kbn/alerts-ui-shared/src/rule_settings/rule_settings_flapping_title_tooltip'; +import { useFetchFlappingSettings } from '@kbn/alerts-ui-shared/src/common/hooks/use_fetch_flapping_settings'; import { useKibana } from '../../../common/lib/kibana'; const alertDelayFormRowLabel = i18n.translate( @@ -66,45 +51,6 @@ const alertDelayAppendLabel = i18n.translate( } ); -const flappingLabel = i18n.translate( - 'xpack.triggersActionsUI.ruleFormAdvancedOptions.flappingLabel', - { - defaultMessage: 'Flapping Detection', - } -); - -const flappingOnLabel = i18n.translate('xpack.triggersActionsUI.ruleFormAdvancedOptions.onLabel', { - defaultMessage: 'ON', -}); - -const flappingOffLabel = i18n.translate( - 'xpack.triggersActionsUI.ruleFormAdvancedOptions.offLabel', - { - defaultMessage: 'OFF', - } -); - -const flappingOverrideLabel = i18n.translate( - 'xpack.triggersActionsUI.ruleFormAdvancedOptions.overrideLabel', - { - defaultMessage: 'Custom', - } -); - -const flappingOverrideConfiguration = i18n.translate( - 'xpack.triggersActionsUI.ruleFormAdvancedOptions.flappingOverrideConfiguration', - { - defaultMessage: 'Override Configuration', - } -); - -const flappingExternalLinkLabel = i18n.translate( - 'xpack.triggersActionsUI.ruleFormAdvancedOptions.flappingExternalLinkLabel', - { - defaultMessage: "What's this?", - } -); - const flappingFormRowLabel = i18n.translate( 'xpack.triggersActionsUI.sections.ruleForm.flappingLabel', { @@ -112,58 +58,13 @@ const flappingFormRowLabel = i18n.translate( } ); -const flappingOffContentRules = i18n.translate( - 'xpack.triggersActionsUI.ruleFormAdvancedOptions.flappingOffContentRules', - { - defaultMessage: 'Rules', - } -); - -const flappingOffContentSettings = i18n.translate( - 'xpack.triggersActionsUI.ruleFormAdvancedOptions.flappingOffContentSettings', - { - defaultMessage: 'Settings', - } -); - -const flappingTitlePopoverFlappingDetection = i18n.translate( - 'xpack.triggersActionsUI.ruleFormAdvancedOptions.flappingTitlePopoverFlappingDetection', - { - defaultMessage: 'flapping detection', - } -); - -const flappingTitlePopoverAlertStatus = i18n.translate( - 'xpack.triggersActionsUI.ruleFormAdvancedOptions.flappingTitlePopoverAlertStatus', - { - defaultMessage: 'alert status change threshold', - } -); - -const flappingTitlePopoverLookBack = i18n.translate( - 'xpack.triggersActionsUI.ruleFormAdvancedOptions.flappingTitlePopoverLookBack', - { - defaultMessage: 'rule run look back window', - } -); - -const clampFlappingValues = (flapping: Rule['flapping']) => { - if (!flapping) { - return; - } - return { - ...flapping, - statusChangeThreshold: Math.min(flapping.lookBackWindow, flapping.statusChangeThreshold), - }; -}; - const INTEGER_REGEX = /^[1-9][0-9]*$/; export interface RuleFormAdvancedOptionsProps { alertDelay?: number; - flappingSettings?: Flapping | null; + flappingSettings?: RuleSpecificFlappingProperties | null; onAlertDelayChange: (value: string) => void; - onFlappingChange: (value: Flapping | null) => void; + onFlappingChange: (value: RuleSpecificFlappingProperties | null) => void; enabledFlapping?: boolean; } @@ -180,20 +81,15 @@ export const RuleFormAdvancedOptions = (props: RuleFormAdvancedOptionsProps) => application: { capabilities: { rulesSettings }, }, + http, } = useKibana().services; - const { writeFlappingSettingsUI = false } = rulesSettings || {}; + const { writeFlappingSettingsUI } = rulesSettings || {}; - const [isFlappingOffPopoverOpen, setIsFlappingOffPopoverOpen] = useState(false); const [isFlappingTitlePopoverOpen, setIsFlappingTitlePopoverOpen] = useState(false); - const cachedFlappingSettings = useRef(); - - const isDesktop = useIsWithinMinBreakpoint('xl'); - - const { euiTheme } = useEuiTheme(); - - const { data: spaceFlappingSettings, isInitialLoading } = useGetFlappingSettings({ + const { data: spaceFlappingSettings, isInitialLoading } = useFetchFlappingSettings({ + http, enabled: enabledFlapping, }); @@ -207,274 +103,6 @@ export const RuleFormAdvancedOptions = (props: RuleFormAdvancedOptionsProps) => [onAlertDelayChange] ); - const internalOnFlappingChange = useCallback( - (flapping: Flapping) => { - const clampedValue = clampFlappingValues(flapping); - if (!clampedValue) { - return; - } - onFlappingChange(clampedValue); - cachedFlappingSettings.current = clampedValue; - }, - [onFlappingChange] - ); - - const onLookBackWindowChange = useCallback( - (value: number) => { - if (!flappingSettings) { - return; - } - internalOnFlappingChange({ - ...flappingSettings, - lookBackWindow: value, - }); - }, - [flappingSettings, internalOnFlappingChange] - ); - - const onStatusChangeThresholdChange = useCallback( - (value: number) => { - if (!flappingSettings) { - return; - } - internalOnFlappingChange({ - ...flappingSettings, - statusChangeThreshold: value, - }); - }, - [flappingSettings, internalOnFlappingChange] - ); - - const onFlappingToggle = useCallback(() => { - if (!spaceFlappingSettings) { - return; - } - if (flappingSettings) { - cachedFlappingSettings.current = flappingSettings; - return onFlappingChange(null); - } - const initialFlappingSettings = cachedFlappingSettings.current || spaceFlappingSettings; - onFlappingChange({ - lookBackWindow: initialFlappingSettings.lookBackWindow, - statusChangeThreshold: initialFlappingSettings.statusChangeThreshold, - }); - }, [spaceFlappingSettings, flappingSettings, onFlappingChange]); - - const flappingTitleTooltip = useMemo(() => { - return ( - setIsFlappingTitlePopoverOpen(false)}> - setIsFlappingTitlePopoverOpen(!isFlappingTitlePopoverOpen)} - /> - } - > - Alert flapping detection - - {flappingTitlePopoverFlappingDetection}, - }} - /> - - - - {flappingTitlePopoverAlertStatus}, - }} - /> - - - - {flappingTitlePopoverLookBack}, - }} - /> - - - - {flappingOffContentRules}, - settings: {flappingOffContentSettings}, - }} - /> - - - - ); - }, [isFlappingTitlePopoverOpen]); - - const flappingOffTooltip = useMemo(() => { - if (!spaceFlappingSettings) { - return null; - } - const { enabled } = spaceFlappingSettings; - if (enabled) { - return null; - } - - if (writeFlappingSettingsUI) { - return ( - setIsFlappingOffPopoverOpen(false)}> - setIsFlappingOffPopoverOpen(!isFlappingOffPopoverOpen)} - /> - } - > - - {flappingOffContentRules}, - settings: {flappingOffContentSettings}, - }} - /> - - - - ); - } - // TODO: Add the external doc link here! - return ( - - {flappingExternalLinkLabel} - - ); - }, [writeFlappingSettingsUI, isFlappingOffPopoverOpen, spaceFlappingSettings]); - - const flappingFormHeader = useMemo(() => { - if (!spaceFlappingSettings) { - return null; - } - const { enabled } = spaceFlappingSettings; - - return ( - - - - - {flappingLabel} - - - {enabled ? flappingOnLabel : flappingOffLabel} - - {flappingSettings && enabled && ( - {flappingOverrideLabel} - )} - - - {enabled && ( - - )} - {flappingOffTooltip} - - - {flappingSettings && enabled && ( - <> - - - - )} - - ); - }, [ - isDesktop, - euiTheme, - spaceFlappingSettings, - flappingSettings, - flappingOffTooltip, - onFlappingToggle, - ]); - - const flappingFormBody = useMemo(() => { - if (!spaceFlappingSettings || !spaceFlappingSettings.enabled) { - return null; - } - if (!flappingSettings) { - return null; - } - return ( - - - - ); - }, [ - flappingSettings, - spaceFlappingSettings, - onLookBackWindowChange, - onStatusChangeThresholdChange, - ]); - - const flappingFormMessage = useMemo(() => { - if (!spaceFlappingSettings || !spaceFlappingSettings.enabled) { - return null; - } - const settingsToUse = flappingSettings || spaceFlappingSettings; - return ( - - - - ); - }, [spaceFlappingSettings, flappingSettings, euiTheme]); - return ( @@ -512,21 +140,23 @@ export const RuleFormAdvancedOptions = (props: RuleFormAdvancedOptionsProps) => label={ {flappingFormRowLabel} - {flappingTitleTooltip} + + + } data-test-subj="alertFlappingFormRow" display="rowCompressed" > - - - - {flappingFormHeader} - {flappingFormBody} - - - {flappingFormMessage} - + )} diff --git a/x-pack/plugins/triggers_actions_ui/public/common/constants/index.ts b/x-pack/plugins/triggers_actions_ui/public/common/constants/index.ts index 2d6548062eed9..ca87ba3522042 100644 --- a/x-pack/plugins/triggers_actions_ui/public/common/constants/index.ts +++ b/x-pack/plugins/triggers_actions_ui/public/common/constants/index.ts @@ -25,9 +25,6 @@ export { I18N_WEEKDAY_OPTIONS_DDD, } from '@kbn/alerts-ui-shared/src/common/constants/i18n_weekdays'; -// Feature flag for frontend rule specific flapping in rule flyout -export const IS_RULE_SPECIFIC_FLAPPING_ENABLED = false; - export const builtInComparators: { [key: string]: Comparator } = { [COMPARATORS.GREATER_THAN]: { text: i18n.translate('xpack.triggersActionsUI.common.constants.comparators.isAboveLabel', { diff --git a/x-pack/test/accessibility/apps/group1/search_profiler.ts b/x-pack/test/accessibility/apps/group1/search_profiler.ts index 522c5e4cf730e..fbd3649120ea1 100644 --- a/x-pack/test/accessibility/apps/group1/search_profiler.ts +++ b/x-pack/test/accessibility/apps/group1/search_profiler.ts @@ -11,7 +11,7 @@ import { FtrProviderContext } from '../../ftr_provider_context'; export default function ({ getService, getPageObjects }: FtrProviderContext) { const PageObjects = getPageObjects(['common', 'security']); const testSubjects = getService('testSubjects'); - const aceEditor = getService('aceEditor'); + const monacoEditor = getService('monacoEditor'); const a11y = getService('a11y'); const esArchiver = getService('esArchiver'); @@ -27,7 +27,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { await esArchiver.unload('x-pack/test/functional/es_archives/logstash_functional'); }); - it('input the JSON in the aceeditor', async () => { + it('input the JSON in the editor', async () => { const input = { query: { bool: { @@ -54,7 +54,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { }, }; - await aceEditor.setValue('searchProfilerEditor', JSON.stringify(input)); + await monacoEditor.setCodeEditorValue(JSON.stringify(input), 0); await a11y.testAppSnapshot(); }); diff --git a/x-pack/test/alerting_api_integration/common/config.ts b/x-pack/test/alerting_api_integration/common/config.ts index fb0194b01be99..3ff3def3f4b70 100644 --- a/x-pack/test/alerting_api_integration/common/config.ts +++ b/x-pack/test/alerting_api_integration/common/config.ts @@ -77,6 +77,7 @@ const enabledActionTypes = [ 'test.system-action', 'test.system-action-kibana-privileges', 'test.system-action-connector-adapter', + 'test.connector-with-hooks', ]; export function createTestConfig(name: string, options: CreateTestConfigOptions) { diff --git a/x-pack/test/alerting_api_integration/common/plugins/alerts/server/action_types.ts b/x-pack/test/alerting_api_integration/common/plugins/alerts/server/action_types.ts index f6903da3c62bc..8d5caf79a4c89 100644 --- a/x-pack/test/alerting_api_integration/common/plugins/alerts/server/action_types.ts +++ b/x-pack/test/alerting_api_integration/common/plugins/alerts/server/action_types.ts @@ -76,6 +76,7 @@ export function defineActionTypes( actions.registerType(getNoAttemptsRateLimitedActionType()); actions.registerType(getAuthorizationActionType(core)); actions.registerType(getExcludedActionType()); + actions.registerType(getHookedActionType()); /** * System actions @@ -139,6 +140,96 @@ function getIndexRecordActionType() { return result; } +function getHookedActionType() { + const paramsSchema = schema.object({}); + type ParamsType = TypeOf; + const configSchema = schema.object({ + index: schema.string(), + source: schema.string(), + }); + type ConfigType = TypeOf; + const secretsSchema = schema.object({ + encrypted: schema.string(), + }); + type SecretsType = TypeOf; + const result: ActionType = { + id: 'test.connector-with-hooks', + name: 'Test: Connector with hooks', + minimumLicenseRequired: 'gold', + supportedFeatureIds: ['alerting'], + validate: { + params: { schema: paramsSchema }, + config: { schema: configSchema }, + secrets: { schema: secretsSchema }, + }, + async executor({ config, secrets, params, services, actionId }) { + return { status: 'ok', actionId }; + }, + async preSaveHook({ connectorId, config, secrets, services, isUpdate, logger }) { + const body = { + state: { + connectorId, + config, + secrets, + isUpdate, + }, + reference: 'pre-save', + source: config.source, + }; + logger.info(`running hook pre-save for ${JSON.stringify(body)}`); + await services.scopedClusterClient.asInternalUser.index({ + index: config.index, + refresh: 'wait_for', + body, + }); + }, + async postSaveHook({ + connectorId, + config, + secrets, + services, + logger, + isUpdate, + wasSuccessful, + }) { + const body = { + state: { + connectorId, + config, + secrets, + isUpdate, + wasSuccessful, + }, + reference: 'post-save', + source: config.source, + }; + logger.info(`running hook post-save for ${JSON.stringify(body)}`); + await services.scopedClusterClient.asInternalUser.index({ + index: config.index, + refresh: 'wait_for', + body, + }); + }, + async postDeleteHook({ connectorId, config, services, logger }) { + const body = { + state: { + connectorId, + config, + }, + reference: 'post-delete', + source: config.source, + }; + logger.info(`running hook post-delete for ${JSON.stringify(body)}`); + await services.scopedClusterClient.asInternalUser.index({ + index: config.index, + refresh: 'wait_for', + body, + }); + }, + }; + return result; +} + function getDelayedActionType() { const paramsSchema = schema.object({ delayInMs: schema.number({ defaultValue: 1000 }), diff --git a/x-pack/test/alerting_api_integration/security_and_spaces/group2/tests/actions/connector_types/openai.ts b/x-pack/test/alerting_api_integration/security_and_spaces/group2/tests/actions/connector_types/openai.ts index 05dfc61dd59e3..8a47b6a882456 100644 --- a/x-pack/test/alerting_api_integration/security_and_spaces/group2/tests/actions/connector_types/openai.ts +++ b/x-pack/test/alerting_api_integration/security_and_spaces/group2/tests/actions/connector_types/openai.ts @@ -147,7 +147,7 @@ export default function genAiTest({ getService }: FtrProviderContext) { statusCode: 400, error: 'Bad Request', message: - 'error validating action type config: types that failed validation:\n- [0.apiProvider]: expected at least one defined value but got [undefined]\n- [1.apiProvider]: expected at least one defined value but got [undefined]', + 'error validating action type config: types that failed validation:\n- [0.apiProvider]: expected at least one defined value but got [undefined]\n- [1.apiProvider]: expected at least one defined value but got [undefined]\n- [2.apiProvider]: expected at least one defined value but got [undefined]', }); }); }); @@ -168,7 +168,7 @@ export default function genAiTest({ getService }: FtrProviderContext) { statusCode: 400, error: 'Bad Request', message: - 'error validating action type config: types that failed validation:\n- [0.apiProvider]: expected value to equal [Azure OpenAI]\n- [1.apiUrl]: expected value of type [string] but got [undefined]', + 'error validating action type config: types that failed validation:\n- [0.apiProvider]: expected value to equal [Azure OpenAI]\n- [1.apiUrl]: expected value of type [string] but got [undefined]\n- [2.apiProvider]: expected value to equal [Other]', }); }); }); diff --git a/x-pack/test/alerting_api_integration/security_and_spaces/group2/tests/actions/create.ts b/x-pack/test/alerting_api_integration/security_and_spaces/group2/tests/actions/create.ts index 017fd3e45999b..e05a1ea9e0350 100644 --- a/x-pack/test/alerting_api_integration/security_and_spaces/group2/tests/actions/create.ts +++ b/x-pack/test/alerting_api_integration/security_and_spaces/group2/tests/actions/create.ts @@ -7,6 +7,7 @@ import { v4 as uuidv4 } from 'uuid'; import expect from '@kbn/expect'; +import { ESTestIndexTool, ES_TEST_INDEX_NAME } from '@kbn/alerting-api-integration-helpers'; import { UserAtSpaceScenarios } from '../../../scenarios'; import { checkAAD, getUrlPrefix, ObjectRemover } from '../../../../common/lib'; import { FtrProviderContext } from '../../../../common/ftr_provider_context'; @@ -15,11 +16,21 @@ import { FtrProviderContext } from '../../../../common/ftr_provider_context'; export default function createActionTests({ getService }: FtrProviderContext) { const supertest = getService('supertest'); const supertestWithoutAuth = getService('supertestWithoutAuth'); + const es = getService('es'); + const retry = getService('retry'); + const esTestIndexTool = new ESTestIndexTool(es, retry); describe('create', () => { const objectRemover = new ObjectRemover(supertest); - after(() => objectRemover.removeAll()); + before(async () => { + await esTestIndexTool.destroy(); + await esTestIndexTool.setup(); + }); + after(async () => { + await esTestIndexTool.destroy(); + await objectRemover.removeAll(); + }); for (const scenario of UserAtSpaceScenarios) { const { user, space } = scenario; @@ -396,6 +407,74 @@ export default function createActionTests({ getService }: FtrProviderContext) { throw new Error(`Scenario untested: ${JSON.stringify(scenario)}`); } }); + + it('should handle save hooks appropriately', async () => { + const source = uuidv4(); + const encryptedValue = 'This value should be encrypted'; + const response = await supertestWithoutAuth + .post(`${getUrlPrefix(space.id)}/api/actions/connector`) + .auth(user.username, user.password) + .set('kbn-xsrf', 'foo') + .send({ + name: 'Hooked action', + connector_type_id: 'test.connector-with-hooks', + config: { + index: ES_TEST_INDEX_NAME, + source, + }, + secrets: { + encrypted: encryptedValue, + }, + }); + + const searchResult = await esTestIndexTool.search(source); + + switch (scenario.id) { + case 'no_kibana_privileges at space1': + case 'global_read at space1': + case 'space_1_all_alerts_none_actions at space1': + case 'space_1_all at space2': + expect(response.statusCode).to.eql(403); + expect(searchResult.body.hits.hits.length).to.eql(0); + break; + case 'superuser at space1': + case 'space_1_all at space1': + case 'space_1_all_with_restricted_fixture at space1': + expect(response.statusCode).to.eql(200); + objectRemover.add(space.id, response.body.id, 'action', 'actions'); + + const refs: string[] = []; + for (const hit of searchResult.body.hits.hits) { + const doc = hit._source as any; + + const reference = doc.reference; + delete doc.reference; + refs.push(reference); + + if (reference === 'post-save') { + expect(doc.state.wasSuccessful).to.be(true); + delete doc.state.wasSuccessful; + } + + const expected = { + state: { + connectorId: response.body.id, + config: { index: ES_TEST_INDEX_NAME, source }, + secrets: { encrypted: encryptedValue }, + isUpdate: false, + }, + source, + }; + expect(doc).to.eql(expected); + } + + refs.sort(); + expect(refs).to.eql(['post-save', 'pre-save']); + break; + default: + throw new Error(`Scenario untested: ${JSON.stringify(scenario)}`); + } + }); }); } }); diff --git a/x-pack/test/alerting_api_integration/security_and_spaces/group2/tests/actions/delete.ts b/x-pack/test/alerting_api_integration/security_and_spaces/group2/tests/actions/delete.ts index b5b11036a3dfd..edb9821418f8d 100644 --- a/x-pack/test/alerting_api_integration/security_and_spaces/group2/tests/actions/delete.ts +++ b/x-pack/test/alerting_api_integration/security_and_spaces/group2/tests/actions/delete.ts @@ -5,7 +5,9 @@ * 2.0. */ +import { v4 as uuidv4 } from 'uuid'; import expect from '@kbn/expect'; +import { ESTestIndexTool, ES_TEST_INDEX_NAME } from '@kbn/alerting-api-integration-helpers'; import { UserAtSpaceScenarios } from '../../../scenarios'; import { getUrlPrefix, ObjectRemover } from '../../../../common/lib'; @@ -15,11 +17,21 @@ import { FtrProviderContext } from '../../../../common/ftr_provider_context'; export default function deleteActionTests({ getService }: FtrProviderContext) { const supertest = getService('supertest'); const supertestWithoutAuth = getService('supertestWithoutAuth'); + const es = getService('es'); + const retry = getService('retry'); + const esTestIndexTool = new ESTestIndexTool(es, retry); describe('delete', () => { const objectRemover = new ObjectRemover(supertest); - after(() => objectRemover.removeAll()); + before(async () => { + await esTestIndexTool.destroy(); + await esTestIndexTool.setup(); + }); + after(async () => { + await esTestIndexTool.destroy(); + await objectRemover.removeAll(); + }); for (const scenario of UserAtSpaceScenarios) { const { user, space } = scenario; @@ -212,6 +224,77 @@ export default function deleteActionTests({ getService }: FtrProviderContext) { throw new Error(`Scenario untested: ${JSON.stringify(scenario)}`); } }); + + it('should handle delete hooks appropriately', async () => { + const source = uuidv4(); + const encryptedValue = 'This value should be encrypted'; + const { body: createdAction } = await supertest + .post(`${getUrlPrefix(space.id)}/api/actions/connector`) + .set('kbn-xsrf', 'foo') + .send({ + name: 'Hooked action', + connector_type_id: 'test.connector-with-hooks', + config: { + index: ES_TEST_INDEX_NAME, + source, + }, + secrets: { + encrypted: encryptedValue, + }, + }) + .expect(200); + + // clear out docs from create + await esTestIndexTool.destroy(); + await esTestIndexTool.setup(); + + const response = await supertestWithoutAuth + .delete(`${getUrlPrefix(space.id)}/api/actions/connector/${createdAction.id}`) + .auth(user.username, user.password) + .set('kbn-xsrf', 'foo'); + + const searchResult = await esTestIndexTool.search(source); + + switch (scenario.id) { + case 'no_kibana_privileges at space1': + case 'global_read at space1': + case 'space_1_all_alerts_none_actions at space1': + case 'space_1_all at space2': + expect(response.statusCode).to.eql(403); + expect(searchResult.body.hits.hits.length).to.eql(0); + objectRemover.add(space.id, createdAction.id, 'action', 'actions'); + break; + case 'superuser at space1': + case 'space_1_all at space1': + case 'space_1_all_with_restricted_fixture at space1': + expect(response.statusCode).to.eql(204); + + const refs: string[] = []; + for (const hit of searchResult.body.hits.hits) { + const doc = hit._source as any; + + const reference = doc.reference; + delete doc.reference; + refs.push(reference); + + const expected = { + state: { + connectorId: createdAction.id, + config: { index: ES_TEST_INDEX_NAME, source }, + }, + source, + }; + expect(doc).to.eql(expected); + } + + refs.sort(); + expect(refs).to.eql(['post-delete']); + break; + default: + objectRemover.add(space.id, createdAction.id, 'action', 'actions'); + throw new Error(`Scenario untested: ${JSON.stringify(scenario)}`); + } + }); }); } }); diff --git a/x-pack/test/alerting_api_integration/security_and_spaces/group2/tests/actions/update.ts b/x-pack/test/alerting_api_integration/security_and_spaces/group2/tests/actions/update.ts index 7c3c00534f11d..cb9fe8a94c8c0 100644 --- a/x-pack/test/alerting_api_integration/security_and_spaces/group2/tests/actions/update.ts +++ b/x-pack/test/alerting_api_integration/security_and_spaces/group2/tests/actions/update.ts @@ -5,7 +5,10 @@ * 2.0. */ +import { v4 as uuidv4 } from 'uuid'; import expect from '@kbn/expect'; +import { ESTestIndexTool, ES_TEST_INDEX_NAME } from '@kbn/alerting-api-integration-helpers'; + import { UserAtSpaceScenarios } from '../../../scenarios'; import { checkAAD, getUrlPrefix, ObjectRemover } from '../../../../common/lib'; import { FtrProviderContext } from '../../../../common/ftr_provider_context'; @@ -14,11 +17,21 @@ import { FtrProviderContext } from '../../../../common/ftr_provider_context'; export default function updateActionTests({ getService }: FtrProviderContext) { const supertest = getService('supertest'); const supertestWithoutAuth = getService('supertestWithoutAuth'); + const es = getService('es'); + const retry = getService('retry'); + const esTestIndexTool = new ESTestIndexTool(es, retry); describe('update', () => { const objectRemover = new ObjectRemover(supertest); - after(() => objectRemover.removeAll()); + before(async () => { + await esTestIndexTool.destroy(); + await esTestIndexTool.setup(); + }); + after(async () => { + await esTestIndexTool.destroy(); + await objectRemover.removeAll(); + }); for (const scenario of UserAtSpaceScenarios) { const { user, space } = scenario; @@ -430,6 +443,94 @@ export default function updateActionTests({ getService }: FtrProviderContext) { throw new Error(`Scenario untested: ${JSON.stringify(scenario)}`); } }); + + it('should handle save hooks appropriately', async () => { + const source = uuidv4(); + const encryptedValue = 'This value should be encrypted'; + + const { body: createdAction } = await supertest + .post(`${getUrlPrefix(space.id)}/api/actions/connector`) + .set('kbn-xsrf', 'foo') + .send({ + name: 'Hooked action', + connector_type_id: 'test.connector-with-hooks', + config: { + index: ES_TEST_INDEX_NAME, + source, + }, + secrets: { + encrypted: encryptedValue, + }, + }) + .expect(200); + objectRemover.add(space.id, createdAction.id, 'action', 'actions'); + + // clear out docs from create + await esTestIndexTool.destroy(); + await esTestIndexTool.setup(); + + const response = await supertestWithoutAuth + .put(`${getUrlPrefix(space.id)}/api/actions/connector/${createdAction.id}`) + .auth(user.username, user.password) + .set('kbn-xsrf', 'foo') + .send({ + name: 'Hooked action', + config: { + index: ES_TEST_INDEX_NAME, + source, + }, + secrets: { + encrypted: encryptedValue, + }, + }); + + const searchResult = await esTestIndexTool.search(source); + + switch (scenario.id) { + case 'no_kibana_privileges at space1': + case 'global_read at space1': + case 'space_1_all_alerts_none_actions at space1': + case 'space_1_all at space2': + expect(response.statusCode).to.eql(403); + expect(searchResult.body.hits.hits.length).to.eql(0); + break; + case 'superuser at space1': + case 'space_1_all at space1': + case 'space_1_all_with_restricted_fixture at space1': + expect(response.statusCode).to.eql(200); + + const refs: string[] = []; + for (const hit of searchResult.body.hits.hits) { + const doc = hit._source as any; + + const reference = doc.reference; + delete doc.reference; + refs.push(reference); + + if (reference === 'post-save') { + expect(doc.state.wasSuccessful).to.be(true); + delete doc.state.wasSuccessful; + } + + const expected = { + state: { + connectorId: response.body.id, + config: { index: ES_TEST_INDEX_NAME, source }, + secrets: { encrypted: encryptedValue }, + isUpdate: true, + }, + source, + }; + expect(doc).to.eql(expected); + } + + refs.sort(); + expect(refs).to.eql(['post-save', 'pre-save']); + break; + default: + throw new Error(`Scenario untested: ${JSON.stringify(scenario)}`); + } + }); }); } }); diff --git a/x-pack/test/api_integration/apis/cloud_security_posture/helper.ts b/x-pack/test/api_integration/apis/cloud_security_posture/helper.ts index 51f98b5389a9d..3ad0ef88ef75a 100644 --- a/x-pack/test/api_integration/apis/cloud_security_posture/helper.ts +++ b/x-pack/test/api_integration/apis/cloud_security_posture/helper.ts @@ -6,59 +6,12 @@ */ import type { Agent as SuperTestAgent } from 'supertest'; -import { Client } from '@elastic/elasticsearch'; -import expect from '@kbn/expect'; + import { ELASTIC_HTTP_VERSION_HEADER } from '@kbn/core-http-common'; -import type { IndexDetails } from '@kbn/cloud-security-posture-common'; import { CLOUD_SECURITY_PLUGIN_VERSION } from '@kbn/cloud-security-posture-plugin/common/constants'; import { SecurityService } from '@kbn/ftr-common-functional-ui-services'; import { RoleCredentials } from '@kbn/ftr-common-functional-services'; -export const deleteIndex = async (es: Client, indexToBeDeleted: string[]) => { - return Promise.all([ - ...indexToBeDeleted.map((indexes) => - es.deleteByQuery({ - index: indexes, - query: { - match_all: {}, - }, - ignore_unavailable: true, - refresh: true, - }) - ), - ]); -}; - -export const bulkIndex = async (es: Client, findingsMock: T[], indexName: string) => { - const operations = findingsMock.flatMap((finding) => [ - { create: { _index: indexName } }, // Action description - { - ...finding, - '@timestamp': new Date().toISOString(), - }, // Data to index - ]); - - await es.bulk({ - body: operations, // Bulk API expects 'body' for operations - refresh: true, - }); -}; - -export const addIndex = async (es: Client, findingsMock: T[], indexName: string) => { - await Promise.all([ - ...findingsMock.map((finding) => - es.index({ - index: indexName, - body: { - ...finding, - '@timestamp': new Date().toISOString(), - }, - refresh: true, - }) - ), - ]); -}; - export async function createPackagePolicy( supertest: SuperTestAgent, agentPolicyId: string, @@ -233,10 +186,10 @@ export const createUser = async (security: SecurityService, userName: string, ro }); }; -export const createCSPOnlyRole = async ( +export const createCSPRole = async ( security: SecurityService, roleName: string, - indicesName: string + indicesName?: string[] ) => { await security.role.create(roleName, { kibana: [ @@ -245,12 +198,12 @@ export const createCSPOnlyRole = async ( spaces: ['*'], }, ], - ...(indicesName.length !== 0 + ...(indicesName && indicesName.length > 0 ? { elasticsearch: { indices: [ { - names: [indicesName], + names: indicesName, privileges: ['read'], }, ], @@ -267,15 +220,3 @@ export const deleteRole = async (security: SecurityService, roleName: string) => export const deleteUser = async (security: SecurityService, userName: string) => { await security.user.delete(userName); }; - -export const assertIndexStatus = ( - indicesDetails: IndexDetails[], - indexName: string, - expectedStatus: string -) => { - const actualValue = indicesDetails.find((idx) => idx.index === indexName)?.status; - expect(actualValue).to.eql( - expectedStatus, - `expected ${indexName} status to be ${expectedStatus} but got ${actualValue} instead` - ); -}; diff --git a/x-pack/test/api_integration/apis/cloud_security_posture/status/status_index_timeout.ts b/x-pack/test/api_integration/apis/cloud_security_posture/status/status_index_timeout.ts index ce0c9014478dc..a2949a9f35253 100644 --- a/x-pack/test/api_integration/apis/cloud_security_posture/status/status_index_timeout.ts +++ b/x-pack/test/api_integration/apis/cloud_security_posture/status/status_index_timeout.ts @@ -13,16 +13,10 @@ import { LATEST_FINDINGS_INDEX_DEFAULT_NS, VULNERABILITIES_INDEX_DEFAULT_NS, } from '@kbn/cloud-security-posture-plugin/common/constants'; +import { EsIndexDataProvider } from '../../../../cloud_security_posture_api/utils'; import { generateAgent } from '../../../../fleet_api_integration/helpers'; import { FtrProviderContext } from '../../../ftr_provider_context'; -import { deleteIndex, createPackagePolicy } from '../helper'; - -const INDEX_ARRAY = [ - FINDINGS_INDEX_DEFAULT_NS, - LATEST_FINDINGS_INDEX_DEFAULT_NS, - CDR_LATEST_NATIVE_VULNERABILITIES_INDEX_PATTERN, - VULNERABILITIES_INDEX_DEFAULT_NS, -]; +import { createPackagePolicy } from '../helper'; const currentTimeMinusFourHours = new Date(Date.now() - 21600000).toISOString(); const currentTimeMinusTenMinutes = new Date(Date.now() - 600000).toISOString(); @@ -35,6 +29,13 @@ export default function (providerContext: FtrProviderContext) { const esArchiver = getService('esArchiver'); const kibanaServer = getService('kibanaServer'); const fleetAndAgents = getService('fleetAndAgents'); + const findingsIndex = new EsIndexDataProvider(es, FINDINGS_INDEX_DEFAULT_NS); + const latestFindingsIndex = new EsIndexDataProvider(es, LATEST_FINDINGS_INDEX_DEFAULT_NS); + const vulnerabilitiesIndex = new EsIndexDataProvider(es, VULNERABILITIES_INDEX_DEFAULT_NS); + const cdrVulnerabilitiesIndex = new EsIndexDataProvider( + es, + CDR_LATEST_NATIVE_VULNERABILITIES_INDEX_PATTERN + ); describe('GET /internal/cloud_security_posture/status', () => { let agentPolicyId: string; @@ -84,12 +85,20 @@ export default function (providerContext: FtrProviderContext) { .expect(200); await generateAgent(providerContext, 'healthy', `Agent policy test 2`, agentPolicyId); - await deleteIndex(es, INDEX_ARRAY); + await findingsIndex.deleteAll(); + await latestFindingsIndex.deleteAll(); + await vulnerabilitiesIndex.deleteAll(); + await cdrVulnerabilitiesIndex.deleteAll(); }); afterEach(async () => { await kibanaServer.savedObjects.cleanStandardList(); await esArchiver.unload('x-pack/test/functional/es_archives/fleet/empty_fleet_server'); + + await findingsIndex.deleteAll(); + await latestFindingsIndex.deleteAll(); + await vulnerabilitiesIndex.deleteAll(); + await cdrVulnerabilitiesIndex.deleteAll(); }); it(`Should return index-timeout when installed kspm, has findings only on logs-cloud_security_posture.findings-default* and it has been more than 10 minutes since the installation`, async () => { diff --git a/x-pack/test/api_integration/apis/cloud_security_posture/status/status_indexed.ts b/x-pack/test/api_integration/apis/cloud_security_posture/status/status_indexed.ts index 504bb9f504516..ec8b6a09f8bb2 100644 --- a/x-pack/test/api_integration/apis/cloud_security_posture/status/status_indexed.ts +++ b/x-pack/test/api_integration/apis/cloud_security_posture/status/status_indexed.ts @@ -8,28 +8,25 @@ import expect from '@kbn/expect'; import { ELASTIC_HTTP_VERSION_HEADER } from '@kbn/core-http-common'; import { CDR_LATEST_NATIVE_VULNERABILITIES_INDEX_PATTERN } from '@kbn/cloud-security-posture-common'; import type { CspSetupStatus } from '@kbn/cloud-security-posture-common'; -import { - FINDINGS_INDEX_DEFAULT_NS, - LATEST_FINDINGS_INDEX_DEFAULT_NS, - VULNERABILITIES_INDEX_DEFAULT_NS, -} from '@kbn/cloud-security-posture-plugin/common/constants'; +import { LATEST_FINDINGS_INDEX_DEFAULT_NS } from '@kbn/cloud-security-posture-plugin/common/constants'; import { FtrProviderContext } from '../../../ftr_provider_context'; -import { deleteIndex, addIndex, createPackagePolicy } from '../helper'; +import { EsIndexDataProvider } from '../../../../cloud_security_posture_api/utils'; +import { createPackagePolicy } from '../helper'; import { findingsMockData, vulnerabilityMockData } from '../mock_data'; -const INDEX_ARRAY = [ - FINDINGS_INDEX_DEFAULT_NS, - LATEST_FINDINGS_INDEX_DEFAULT_NS, - CDR_LATEST_NATIVE_VULNERABILITIES_INDEX_PATTERN, - VULNERABILITIES_INDEX_DEFAULT_NS, -]; - export default function (providerContext: FtrProviderContext) { const { getService } = providerContext; const supertest = getService('supertest'); const es = getService('es'); const esArchiver = getService('esArchiver'); const kibanaServer = getService('kibanaServer'); + const latestFindingsIndex = new EsIndexDataProvider(es, LATEST_FINDINGS_INDEX_DEFAULT_NS); + const latestVulnerabilitiesIndex = new EsIndexDataProvider( + es, + CDR_LATEST_NATIVE_VULNERABILITIES_INDEX_PATTERN + ); + const mock3PIndex = 'security_solution-mock-3p-integration.misconfiguration_latest'; + const _3pIndex = new EsIndexDataProvider(es, mock3PIndex); describe('GET /internal/cloud_security_posture/status', () => { let agentPolicyId: string; @@ -50,19 +47,21 @@ export default function (providerContext: FtrProviderContext) { agentPolicyId = agentPolicyResponse.item.id; - await deleteIndex(es, INDEX_ARRAY); - await addIndex(es, findingsMockData, LATEST_FINDINGS_INDEX_DEFAULT_NS); - await addIndex(es, vulnerabilityMockData, CDR_LATEST_NATIVE_VULNERABILITIES_INDEX_PATTERN); + await latestFindingsIndex.deleteAll(); + await latestVulnerabilitiesIndex.deleteAll(); + await _3pIndex.deleteAll(); }); afterEach(async () => { - await deleteIndex(es, INDEX_ARRAY); + await latestFindingsIndex.deleteAll(); + await latestVulnerabilitiesIndex.deleteAll(); + await _3pIndex.destroyIndex(); await kibanaServer.savedObjects.cleanStandardList(); await esArchiver.unload('x-pack/test/functional/es_archives/fleet/empty_fleet_server'); }); it(`Return hasMisconfigurationsFindings true when there are latest findings but no installed integrations`, async () => { - await addIndex(es, findingsMockData, LATEST_FINDINGS_INDEX_DEFAULT_NS); + await latestFindingsIndex.addBulk(findingsMockData); const { body: res }: { body: CspSetupStatus } = await supertest .get(`/internal/cloud_security_posture/status`) @@ -77,9 +76,7 @@ export default function (providerContext: FtrProviderContext) { }); it(`Return hasMisconfigurationsFindings true when there are only findings in third party index`, async () => { - await deleteIndex(es, INDEX_ARRAY); - const mock3PIndex = 'security_solution-mock-3p-integration.misconfiguration_latest'; - await addIndex(es, findingsMockData, mock3PIndex); + await _3pIndex.addBulk(findingsMockData); const { body: res }: { body: CspSetupStatus } = await supertest .get(`/internal/cloud_security_posture/status`) @@ -91,13 +88,9 @@ export default function (providerContext: FtrProviderContext) { true, `expected hasMisconfigurationsFindings to be true but got ${res.hasMisconfigurationsFindings} instead` ); - - await deleteIndex(es, [mock3PIndex]); }); it(`Return hasMisconfigurationsFindings false when there are no findings`, async () => { - await deleteIndex(es, INDEX_ARRAY); - const { body: res }: { body: CspSetupStatus } = await supertest .get(`/internal/cloud_security_posture/status`) .set(ELASTIC_HTTP_VERSION_HEADER, '1') @@ -120,6 +113,8 @@ export default function (providerContext: FtrProviderContext) { 'kspm' ); + await latestFindingsIndex.addBulk(findingsMockData); + const { body: res }: { body: CspSetupStatus } = await supertest .get(`/internal/cloud_security_posture/status`) .set(ELASTIC_HTTP_VERSION_HEADER, '1') @@ -142,6 +137,8 @@ export default function (providerContext: FtrProviderContext) { 'cspm' ); + await latestFindingsIndex.addBulk(findingsMockData); + const { body: res }: { body: CspSetupStatus } = await supertest .get(`/internal/cloud_security_posture/status`) .set(ELASTIC_HTTP_VERSION_HEADER, '1') @@ -164,6 +161,8 @@ export default function (providerContext: FtrProviderContext) { 'vuln_mgmt' ); + await latestVulnerabilitiesIndex.addBulk(vulnerabilityMockData); + const { body: res }: { body: CspSetupStatus } = await supertest .get(`/internal/cloud_security_posture/status`) .set(ELASTIC_HTTP_VERSION_HEADER, '1') diff --git a/x-pack/test/api_integration/apis/cloud_security_posture/status/status_indexing.ts b/x-pack/test/api_integration/apis/cloud_security_posture/status/status_indexing.ts index 4d66d8460b9a4..16ee02083e34c 100644 --- a/x-pack/test/api_integration/apis/cloud_security_posture/status/status_indexing.ts +++ b/x-pack/test/api_integration/apis/cloud_security_posture/status/status_indexing.ts @@ -7,29 +7,23 @@ import expect from '@kbn/expect'; import { ELASTIC_HTTP_VERSION_HEADER } from '@kbn/core-http-common'; import type { CspSetupStatus } from '@kbn/cloud-security-posture-common'; -import { CDR_LATEST_NATIVE_VULNERABILITIES_INDEX_PATTERN } from '@kbn/cloud-security-posture-common'; import { FINDINGS_INDEX_DEFAULT_NS, - LATEST_FINDINGS_INDEX_DEFAULT_NS, VULNERABILITIES_INDEX_DEFAULT_NS, } from '@kbn/cloud-security-posture-plugin/common/constants'; import { FtrProviderContext } from '../../../ftr_provider_context'; -import { deleteIndex, addIndex, createPackagePolicy } from '../helper'; +import { EsIndexDataProvider } from '../../../../cloud_security_posture_api/utils'; +import { createPackagePolicy } from '../helper'; import { findingsMockData, vulnerabilityMockData } from '../mock_data'; -const INDEX_ARRAY = [ - FINDINGS_INDEX_DEFAULT_NS, - LATEST_FINDINGS_INDEX_DEFAULT_NS, - CDR_LATEST_NATIVE_VULNERABILITIES_INDEX_PATTERN, - VULNERABILITIES_INDEX_DEFAULT_NS, -]; - export default function (providerContext: FtrProviderContext) { const { getService } = providerContext; const supertest = getService('supertest'); const es = getService('es'); const esArchiver = getService('esArchiver'); const kibanaServer = getService('kibanaServer'); + const findingsIndex = new EsIndexDataProvider(es, FINDINGS_INDEX_DEFAULT_NS); + const vulnerabilitiesIndex = new EsIndexDataProvider(es, VULNERABILITIES_INDEX_DEFAULT_NS); describe('GET /internal/cloud_security_posture/status', () => { let agentPolicyId: string; @@ -49,13 +43,13 @@ export default function (providerContext: FtrProviderContext) { }); agentPolicyId = agentPolicyResponse.item.id; - await deleteIndex(es, INDEX_ARRAY); - await addIndex(es, findingsMockData, FINDINGS_INDEX_DEFAULT_NS); - await addIndex(es, vulnerabilityMockData, VULNERABILITIES_INDEX_DEFAULT_NS); + await findingsIndex.deleteAll(); + await vulnerabilitiesIndex.deleteAll(); }); afterEach(async () => { - await deleteIndex(es, INDEX_ARRAY); + await findingsIndex.deleteAll(); + await vulnerabilitiesIndex.deleteAll(); await kibanaServer.savedObjects.cleanStandardList(); await esArchiver.unload('x-pack/test/functional/es_archives/fleet/empty_fleet_server'); }); @@ -70,6 +64,8 @@ export default function (providerContext: FtrProviderContext) { 'kspm' ); + await findingsIndex.addBulk(findingsMockData); + const { body: res }: { body: CspSetupStatus } = await supertest .get(`/internal/cloud_security_posture/status`) .set(ELASTIC_HTTP_VERSION_HEADER, '1') @@ -92,6 +88,8 @@ export default function (providerContext: FtrProviderContext) { 'cspm' ); + await findingsIndex.addBulk(findingsMockData); + const { body: res }: { body: CspSetupStatus } = await supertest .get(`/internal/cloud_security_posture/status`) .set(ELASTIC_HTTP_VERSION_HEADER, '1') @@ -114,6 +112,8 @@ export default function (providerContext: FtrProviderContext) { 'vuln_mgmt' ); + await vulnerabilitiesIndex.addBulk(vulnerabilityMockData); + const { body: res }: { body: CspSetupStatus } = await supertest .get(`/internal/cloud_security_posture/status`) .set(ELASTIC_HTTP_VERSION_HEADER, '1') diff --git a/x-pack/test/api_integration/apis/cloud_security_posture/status/status_unprivileged.ts b/x-pack/test/api_integration/apis/cloud_security_posture/status/status_unprivileged.ts index 7c09e4b51f679..5d0f6207e904a 100644 --- a/x-pack/test/api_integration/apis/cloud_security_posture/status/status_unprivileged.ts +++ b/x-pack/test/api_integration/apis/cloud_security_posture/status/status_unprivileged.ts @@ -13,16 +13,9 @@ import { LATEST_FINDINGS_INDEX_DEFAULT_NS, FINDINGS_INDEX_PATTERN, } from '@kbn/cloud-security-posture-plugin/common/constants'; +import { find, without } from 'lodash'; import { FtrProviderContext } from '../../../ftr_provider_context'; -import { - createPackagePolicy, - createUser, - createCSPOnlyRole, - deleteRole, - deleteUser, - deleteIndex, - assertIndexStatus, -} from '../helper'; +import { createPackagePolicy, createUser, createCSPRole, deleteRole, deleteUser } from '../helper'; const UNPRIVILEGED_ROLE = 'unprivileged_test_role'; const UNPRIVILEGED_USERNAME = 'unprivileged_test_user'; @@ -32,27 +25,36 @@ export default function (providerContext: FtrProviderContext) { const supertest = getService('supertest'); const supertestWithoutAuth = getService('supertestWithoutAuth'); const esArchiver = getService('esArchiver'); - const es = getService('es'); const kibanaServer = getService('kibanaServer'); const security = getService('security'); + const allIndices = [ + LATEST_FINDINGS_INDEX_DEFAULT_NS, + FINDINGS_INDEX_PATTERN, + BENCHMARK_SCORE_INDEX_DEFAULT_NS, + CDR_LATEST_NATIVE_VULNERABILITIES_INDEX_PATTERN, + ]; + describe('GET /internal/cloud_security_posture/status', () => { let agentPolicyId: string; describe('STATUS = UNPRIVILEGED TEST', () => { before(async () => { - await createCSPOnlyRole(security, UNPRIVILEGED_ROLE, ''); + await createCSPRole(security, UNPRIVILEGED_ROLE); await createUser(security, UNPRIVILEGED_USERNAME, UNPRIVILEGED_ROLE); + await esArchiver.loadIfNeeded( + 'x-pack/test/functional/es_archives/fleet/empty_fleet_server' + ); }); after(async () => { await deleteUser(security, UNPRIVILEGED_USERNAME); await deleteRole(security, UNPRIVILEGED_ROLE); + await esArchiver.unload('x-pack/test/functional/es_archives/fleet/empty_fleet_server'); }); beforeEach(async () => { await kibanaServer.savedObjects.cleanStandardList(); - await esArchiver.load('x-pack/test/functional/es_archives/fleet/empty_fleet_server'); const { body: agentPolicyResponse } = await supertest .post(`/api/fleet/agent_policies`) @@ -67,7 +69,6 @@ export default function (providerContext: FtrProviderContext) { }); afterEach(async () => { - await esArchiver.unload('x-pack/test/functional/es_archives/fleet/empty_fleet_server'); await kibanaServer.savedObjects.cleanStandardList(); }); @@ -106,7 +107,6 @@ export default function (providerContext: FtrProviderContext) { describe('status = unprivileged test indices', () => { beforeEach(async () => { await kibanaServer.savedObjects.cleanStandardList(); - await esArchiver.load('x-pack/test/functional/es_archives/fleet/empty_fleet_server'); const { body: agentPolicyResponse } = await supertest .post(`/api/fleet/agent_policies`) @@ -124,11 +124,21 @@ export default function (providerContext: FtrProviderContext) { await deleteUser(security, UNPRIVILEGED_USERNAME); await deleteRole(security, UNPRIVILEGED_ROLE); await kibanaServer.savedObjects.cleanStandardList(); + }); + + before(async () => { + await esArchiver.loadIfNeeded( + 'x-pack/test/functional/es_archives/fleet/empty_fleet_server' + ); + }); + + after(async () => { await esArchiver.unload('x-pack/test/functional/es_archives/fleet/empty_fleet_server'); }); it(`Return unprivileged when missing access to findings_latest index`, async () => { - await createCSPOnlyRole(security, UNPRIVILEGED_ROLE, LATEST_FINDINGS_INDEX_DEFAULT_NS); + const privilegedIndices = without(allIndices, LATEST_FINDINGS_INDEX_DEFAULT_NS); + await createCSPRole(security, UNPRIVILEGED_ROLE, privilegedIndices); await createUser(security, UNPRIVILEGED_USERNAME, UNPRIVILEGED_ROLE); await createPackagePolicy( @@ -149,30 +159,30 @@ export default function (providerContext: FtrProviderContext) { expect(res.kspm.status).to.eql( 'unprivileged', - `expected unprivileged but got ${res.kspm.status} instead` + `kspm status expected unprivileged but got ${res.kspm.status} instead` ); expect(res.cspm.status).to.eql( 'unprivileged', - `expected unprivileged but got ${res.cspm.status} instead` + `cspm status expected unprivileged but got ${res.cspm.status} instead` ); expect(res.vuln_mgmt.status).to.eql( - 'unprivileged', - `expected unprivileged but got ${res.vuln_mgmt.status} instead` + 'not-installed', + `cnvm status expected not_installed but got ${res.vuln_mgmt.status} instead` ); - assertIndexStatus(res.indicesDetails, LATEST_FINDINGS_INDEX_DEFAULT_NS, 'empty'); - assertIndexStatus(res.indicesDetails, FINDINGS_INDEX_PATTERN, 'empty'); - assertIndexStatus(res.indicesDetails, BENCHMARK_SCORE_INDEX_DEFAULT_NS, 'unprivileged'); - assertIndexStatus( - res.indicesDetails, - CDR_LATEST_NATIVE_VULNERABILITIES_INDEX_PATTERN, + expect(res).to.have.property('indicesDetails'); + expect(find(res.indicesDetails, { index: LATEST_FINDINGS_INDEX_DEFAULT_NS })?.status).eql( 'unprivileged' ); + + privilegedIndices.forEach((index) => { + expect(find(res.indicesDetails, { index })?.status).not.eql('unprivileged'); + }); }); it(`Return unprivileged when missing access to score index`, async () => { - await deleteIndex(es, [BENCHMARK_SCORE_INDEX_DEFAULT_NS]); - await createCSPOnlyRole(security, UNPRIVILEGED_ROLE, BENCHMARK_SCORE_INDEX_DEFAULT_NS); + const privilegedIndices = without(allIndices, BENCHMARK_SCORE_INDEX_DEFAULT_NS); + await createCSPRole(security, UNPRIVILEGED_ROLE, privilegedIndices); await createUser(security, UNPRIVILEGED_USERNAME, UNPRIVILEGED_ROLE); await createPackagePolicy( @@ -193,33 +203,33 @@ export default function (providerContext: FtrProviderContext) { expect(res.kspm.status).to.eql( 'unprivileged', - `expected unprivileged but got ${res.kspm.status} instead` + `kspm status expected unprivileged but got ${res.kspm.status} instead` ); expect(res.cspm.status).to.eql( 'unprivileged', - `expected unprivileged but got ${res.cspm.status} instead` + `cspm status expected unprivileged but got ${res.cspm.status} instead` ); expect(res.vuln_mgmt.status).to.eql( 'unprivileged', - `expected unprivileged but got ${res.vuln_mgmt.status} instead` + `cnvm status expected unprivileged but got ${res.vuln_mgmt.status} instead` ); - assertIndexStatus(res.indicesDetails, LATEST_FINDINGS_INDEX_DEFAULT_NS, 'unprivileged'); - assertIndexStatus(res.indicesDetails, FINDINGS_INDEX_PATTERN, 'empty'); - assertIndexStatus(res.indicesDetails, BENCHMARK_SCORE_INDEX_DEFAULT_NS, 'empty'); - assertIndexStatus( - res.indicesDetails, - CDR_LATEST_NATIVE_VULNERABILITIES_INDEX_PATTERN, + expect(res).to.have.property('indicesDetails'); + expect(find(res.indicesDetails, { index: BENCHMARK_SCORE_INDEX_DEFAULT_NS })?.status).eql( 'unprivileged' ); + + privilegedIndices.forEach((index) => { + expect(find(res.indicesDetails, { index })?.status).not.eql('unprivileged'); + }); }); it(`Return unprivileged when missing access to vulnerabilities_latest index`, async () => { - await createCSPOnlyRole( - security, - UNPRIVILEGED_ROLE, + const privilegedIndices = without( + allIndices, CDR_LATEST_NATIVE_VULNERABILITIES_INDEX_PATTERN ); + await createCSPRole(security, UNPRIVILEGED_ROLE, privilegedIndices); await createUser(security, UNPRIVILEGED_USERNAME, UNPRIVILEGED_ROLE); await createPackagePolicy( @@ -239,26 +249,27 @@ export default function (providerContext: FtrProviderContext) { .expect(200); expect(res.kspm.status).to.eql( - 'unprivileged', - `expected unprivileged but got ${res.kspm.status} instead` + 'not-deployed', + `kspm status expected unprivileged but got ${res.kspm.status} instead` ); expect(res.cspm.status).to.eql( - 'unprivileged', - `expected unprivileged but got ${res.cspm.status} instead` + 'not-installed', + `cspm status expected unprivileged but got ${res.cspm.status} instead` ); expect(res.vuln_mgmt.status).to.eql( 'unprivileged', - `expected unprivileged but got ${res.vuln_mgmt.status} instead` + `cnvm status expected unprivileged but got ${res.vuln_mgmt.status} instead` ); - assertIndexStatus(res.indicesDetails, LATEST_FINDINGS_INDEX_DEFAULT_NS, 'unprivileged'); - assertIndexStatus(res.indicesDetails, FINDINGS_INDEX_PATTERN, 'empty'); - assertIndexStatus(res.indicesDetails, BENCHMARK_SCORE_INDEX_DEFAULT_NS, 'unprivileged'); - assertIndexStatus( - res.indicesDetails, - CDR_LATEST_NATIVE_VULNERABILITIES_INDEX_PATTERN, - 'empty' - ); + expect(res).to.have.property('indicesDetails'); + expect( + find(res.indicesDetails, { index: CDR_LATEST_NATIVE_VULNERABILITIES_INDEX_PATTERN }) + ?.status + ).eql('unprivileged'); + + privilegedIndices.forEach((index) => { + expect(find(res.indicesDetails, { index })?.status).not.eql('unprivileged'); + }); }); }); }); diff --git a/x-pack/test/api_integration/apis/entity_manager/definitions.ts b/x-pack/test/api_integration/apis/entity_manager/definitions.ts index 466b5e0232bf0..b51a26ad7b5ad 100644 --- a/x-pack/test/api_integration/apis/entity_manager/definitions.ts +++ b/x-pack/test/api_integration/apis/entity_manager/definitions.ts @@ -8,10 +8,7 @@ import semver from 'semver'; import expect from '@kbn/expect'; import { entityLatestSchema } from '@kbn/entities-schema'; -import { - entityDefinition as mockDefinition, - entityDefinitionWithBackfill as mockBackfillDefinition, -} from '@kbn/entityManager-plugin/server/lib/entities/helpers/fixtures'; +import { entityDefinition as mockDefinition } from '@kbn/entityManager-plugin/server/lib/entities/helpers/fixtures'; import { PartialConfig, cleanup, generate } from '@kbn/data-forge'; import { generateLatestIndexName } from '@kbn/entityManager-plugin/server/lib/entities/helpers/generate_component_id'; import { FtrProviderContext } from '../../ftr_provider_context'; @@ -33,8 +30,9 @@ export default function ({ getService }: FtrProviderContext) { describe('Entity definitions', () => { describe('definitions installations', () => { it('can install multiple definitions', async () => { + const mockDefinitionDup = { ...mockDefinition, id: 'mock_definition_dup' }; await installDefinition(supertest, { definition: mockDefinition }); - await installDefinition(supertest, { definition: mockBackfillDefinition }); + await installDefinition(supertest, { definition: mockDefinitionDup }); const { definitions } = await getInstalledDefinitions(supertest); expect(definitions.length).to.eql(2); @@ -49,7 +47,7 @@ export default function ({ getService }: FtrProviderContext) { expect( definitions.some( (definition) => - definition.id === mockBackfillDefinition.id && + definition.id === mockDefinitionDup.id && definition.state.installed === true && definition.state.running === true ) @@ -57,7 +55,7 @@ export default function ({ getService }: FtrProviderContext) { await Promise.all([ uninstallDefinition(supertest, { id: mockDefinition.id, deleteData: true }), - uninstallDefinition(supertest, { id: mockBackfillDefinition.id, deleteData: true }), + uninstallDefinition(supertest, { id: mockDefinitionDup.id, deleteData: true }), ]); }); @@ -89,7 +87,7 @@ export default function ({ getService }: FtrProviderContext) { id: mockDefinition.id, update: { version: incVersion!, - history: { + latest: { timestampField: '@updatedTimestampField', }, }, @@ -99,7 +97,7 @@ export default function ({ getService }: FtrProviderContext) { definitions: [updatedDefinition], } = await getInstalledDefinitions(supertest); expect(updatedDefinition.version).to.eql(incVersion); - expect(updatedDefinition.history.timestampField).to.eql('@updatedTimestampField'); + expect(updatedDefinition.latest.timestampField).to.eql('@updatedTimestampField'); await uninstallDefinition(supertest, { id: mockDefinition.id }); }); @@ -114,7 +112,7 @@ export default function ({ getService }: FtrProviderContext) { id: mockDefinition.id, update: { version: '1.0.0', - history: { + latest: { timestampField: '@updatedTimestampField', }, }, diff --git a/x-pack/test/api_integration/apis/management/index_management/data_enrichers/ilm.ts b/x-pack/test/api_integration/apis/management/index_management/data_enrichers/ilm.ts index 234a1518a9c59..3ae9b554bf3ee 100644 --- a/x-pack/test/api_integration/apis/management/index_management/data_enrichers/ilm.ts +++ b/x-pack/test/api_integration/apis/management/index_management/data_enrichers/ilm.ts @@ -55,16 +55,17 @@ export default function ({ getService }: FtrProviderContext) { const testAlias = 'test_alias'; const testIlmPolicy = 'test_policy'; describe('GET indices with data enrichers', () => { - before(async () => { + beforeEach(async () => { await createIndex(testIndex); - await createIlmPolicy('test_policy'); - await addPolicyToIndex(testIlmPolicy, testIndex, testAlias); }); - after(async () => { + afterEach(async () => { await esDeleteAllIndices([testIndex]); }); it(`ILM data is fetched by the ILM data enricher`, async () => { + await createIlmPolicy('test_policy'); + await addPolicyToIndex(testIlmPolicy, testIndex, testAlias); + const { body: indices } = await supertest .get(`${API_BASE_PATH}/indices`) .set('kbn-xsrf', 'xxx') @@ -75,5 +76,18 @@ export default function ({ getService }: FtrProviderContext) { const { ilm } = index; expect(ilm.policy).to.eql(testIlmPolicy); }); + + it(`ILM data is not empty even if the index unmanaged`, async () => { + const { body: indices } = await supertest + .get(`${API_BASE_PATH}/indices`) + .set('kbn-xsrf', 'xxx') + .expect(200); + + const index = indices.find((item: Index) => item.name === testIndex); + + const { ilm } = index; + expect(ilm.index).to.eql(testIndex); + expect(ilm.managed).to.eql(false); + }); }); } diff --git a/x-pack/test/api_integration/apis/ml/anomaly_detectors/forecast_with_spaces.ts b/x-pack/test/api_integration/apis/ml/anomaly_detectors/forecast_with_spaces.ts index 1176452408762..32e82c67e348d 100644 --- a/x-pack/test/api_integration/apis/ml/anomaly_detectors/forecast_with_spaces.ts +++ b/x-pack/test/api_integration/apis/ml/anomaly_detectors/forecast_with_spaces.ts @@ -38,7 +38,29 @@ export default ({ getService }: FtrProviderContext) => { return body; } - describe('POST anomaly_detectors _forecast with spaces', function () { + async function deleteForecast( + jobId: string, + forecastId: string, + space: string, + user: USER, + expectedStatusCode: number + ) { + const { body, status } = await supertest + .delete( + `${ + space ? `/s/${space}` : '' + }/internal/ml/anomaly_detectors/${jobId}/_forecast/${forecastId}` + ) + .auth(user, ml.securityCommon.getPasswordForUser(user)) + .set(getCommonRequestHeader('1')); + ml.api.assertResponseStatusCode(expectedStatusCode, status, body); + + return body; + } + + // Failing see: https://github.com/elastic/kibana/issues/195602 + describe.skip('POST anomaly_detectors _forecast with spaces', function () { + let forecastId: string; before(async () => { await esArchiver.loadIfNeeded('x-pack/test/functional/es_archives/ml/farequote'); await ml.testResources.setKibanaTimeZoneToUTC(); @@ -79,13 +101,22 @@ export default ({ getService }: FtrProviderContext) => { await ml.api.waitForDatafeedState(forecastJobDatafeedId, DATAFEED_STATE.STOPPED); await ml.api.waitForJobState(forecastJobId, JOB_STATE.CLOSED); await ml.api.openAnomalyDetectionJob(forecastJobId); - await runForecast(forecastJobId, idSpace1, '1d', USER.ML_POWERUSER, 200); + const resp = await runForecast(forecastJobId, idSpace1, '1d', USER.ML_POWERUSER, 200); + forecastId = resp.forecast_id; await ml.testExecution.logTestStep( `forecast results should exist for job '${forecastJobId}'` ); await ml.api.assertForecastResultsExist(forecastJobId); }); + it('should not delete forecast for user without permissions', async () => { + await await deleteForecast(forecastJobId, forecastId, idSpace1, USER.ML_VIEWER, 403); + }); + + it('should delete forecast for user with permissions', async () => { + await await deleteForecast(forecastJobId, forecastId, idSpace1, USER.ML_POWERUSER, 200); + }); + it('should not run forecast for open job with invalid duration', async () => { await runForecast(forecastJobId, idSpace1, 3600000, USER.ML_POWERUSER, 400); }); diff --git a/x-pack/test/api_integration/apis/ml/system/capabilities.ts b/x-pack/test/api_integration/apis/ml/system/capabilities.ts index b653632432310..c4775cacdfa66 100644 --- a/x-pack/test/api_integration/apis/ml/system/capabilities.ts +++ b/x-pack/test/api_integration/apis/ml/system/capabilities.ts @@ -12,7 +12,7 @@ import { FtrProviderContext } from '../../../ftr_provider_context'; import { getCommonRequestHeader } from '../../../../functional/services/ml/common_api'; import { USER } from '../../../../functional/services/ml/security_common'; -const NUMBER_OF_CAPABILITIES = 43; +const NUMBER_OF_CAPABILITIES = 44; export default ({ getService }: FtrProviderContext) => { const supertest = getService('supertestWithoutAuth'); @@ -61,6 +61,7 @@ export default ({ getService }: FtrProviderContext) => { canResetJob: false, canUpdateJob: false, canForecastJob: false, + canDeleteForecast: false, canCreateDatafeed: false, canDeleteDatafeed: false, canStartStopDatafeed: false, @@ -111,6 +112,7 @@ export default ({ getService }: FtrProviderContext) => { canResetJob: true, canUpdateJob: true, canForecastJob: true, + canDeleteForecast: true, canCreateDatafeed: true, canDeleteDatafeed: true, canStartStopDatafeed: true, diff --git a/x-pack/test/api_integration/apis/ml/system/space_capabilities.ts b/x-pack/test/api_integration/apis/ml/system/space_capabilities.ts index f45d54a741da3..1832e5d096e34 100644 --- a/x-pack/test/api_integration/apis/ml/system/space_capabilities.ts +++ b/x-pack/test/api_integration/apis/ml/system/space_capabilities.ts @@ -15,7 +15,7 @@ import { USER } from '../../../../functional/services/ml/security_common'; const idSpaceWithMl = 'space_with_ml'; const idSpaceNoMl = 'space_no_ml'; -const NUMBER_OF_CAPABILITIES = 43; +const NUMBER_OF_CAPABILITIES = 44; export default ({ getService }: FtrProviderContext) => { const supertest = getService('supertestWithoutAuth'); @@ -90,6 +90,7 @@ export default ({ getService }: FtrProviderContext) => { canResetJob: false, canUpdateJob: false, canForecastJob: false, + canDeleteForecast: false, canCreateDatafeed: false, canDeleteDatafeed: false, canStartStopDatafeed: false, @@ -139,6 +140,7 @@ export default ({ getService }: FtrProviderContext) => { canResetJob: false, canUpdateJob: false, canForecastJob: false, + canDeleteForecast: false, canCreateDatafeed: false, canDeleteDatafeed: false, canStartStopDatafeed: false, @@ -188,6 +190,7 @@ export default ({ getService }: FtrProviderContext) => { canResetJob: true, canUpdateJob: true, canForecastJob: true, + canDeleteForecast: true, canCreateDatafeed: true, canDeleteDatafeed: true, canStartStopDatafeed: true, @@ -237,6 +240,7 @@ export default ({ getService }: FtrProviderContext) => { canResetJob: false, canUpdateJob: false, canForecastJob: false, + canDeleteForecast: false, canCreateDatafeed: false, canDeleteDatafeed: false, canStartStopDatafeed: false, diff --git a/x-pack/test/api_integration/apis/ml/trained_models/model_downloads.ts b/x-pack/test/api_integration/apis/ml/trained_models/model_downloads.ts index 4e229c133b4fd..4e5fd70314495 100644 --- a/x-pack/test/api_integration/apis/ml/trained_models/model_downloads.ts +++ b/x-pack/test/api_integration/apis/ml/trained_models/model_downloads.ts @@ -100,6 +100,8 @@ export default ({ getService }: FtrProviderContext) => { }, }, description: 'E5 (EmbEddings from bidirEctional Encoder rEpresentations)', + disclaimer: + 'This E5 model, as defined, hosted, integrated and used in conjunction with our other Elastic Software is covered by our standard warranty.', license: 'MIT', licenseUrl: 'https://huggingface.co/elastic/multilingual-e5-small', type: ['pytorch', 'text_embedding'], @@ -119,6 +121,8 @@ export default ({ getService }: FtrProviderContext) => { }, description: 'E5 (EmbEddings from bidirEctional Encoder rEpresentations), optimized for linux-x86_64', + disclaimer: + 'This E5 model, as defined, hosted, integrated and used in conjunction with our other Elastic Software is covered by our standard warranty.', license: 'MIT', licenseUrl: 'https://huggingface.co/elastic/multilingual-e5-small_linux-x86_64', type: ['pytorch', 'text_embedding'], diff --git a/x-pack/test/api_integration/deployment_agnostic/apis/observability/dataset_quality/degraded_field_analyze.ts b/x-pack/test/api_integration/deployment_agnostic/apis/observability/dataset_quality/degraded_field_analyze.ts index 056bde27fc33c..6dc8af72bea81 100644 --- a/x-pack/test/api_integration/deployment_agnostic/apis/observability/dataset_quality/degraded_field_analyze.ts +++ b/x-pack/test/api_integration/deployment_agnostic/apis/observability/dataset_quality/degraded_field_analyze.ts @@ -45,7 +45,9 @@ export default function ({ getService }: DeploymentAgnosticFtrProviderContext) { .query({ lastBackingIndex }); } - describe('Degraded field analyze', () => { + describe('Degraded field analyze', function () { + // see details: https://github.com/elastic/kibana/issues/195466 + this.tags(['failsOnMKI']); let supertestAdminWithCookieCredentials: SupertestWithRoleScopeType; before(async () => { diff --git a/x-pack/test/api_integration/deployment_agnostic/configs/serverless/oblt.index.ts b/x-pack/test/api_integration/deployment_agnostic/configs/serverless/oblt.index.ts index f734f0b805d85..b11ced857253f 100644 --- a/x-pack/test/api_integration/deployment_agnostic/configs/serverless/oblt.index.ts +++ b/x-pack/test/api_integration/deployment_agnostic/configs/serverless/oblt.index.ts @@ -7,7 +7,9 @@ import { DeploymentAgnosticFtrProviderContext } from '../../ftr_provider_context'; export default function ({ loadTestFile }: DeploymentAgnosticFtrProviderContext) { - describe('Serverless Observability - Deployment-agnostic api integration tests', () => { + describe('Serverless Observability - Deployment-agnostic api integration tests', function () { + this.tags(['esGate']); + // load new oblt and platform deployment-agnostic test here loadTestFile(require.resolve('../../apis/console')); loadTestFile(require.resolve('../../apis/core')); diff --git a/x-pack/test/api_integration/deployment_agnostic/configs/serverless/search.index.ts b/x-pack/test/api_integration/deployment_agnostic/configs/serverless/search.index.ts index 468cafccccf1d..97db4bf32d47a 100644 --- a/x-pack/test/api_integration/deployment_agnostic/configs/serverless/search.index.ts +++ b/x-pack/test/api_integration/deployment_agnostic/configs/serverless/search.index.ts @@ -7,7 +7,9 @@ import { DeploymentAgnosticFtrProviderContext } from '../../ftr_provider_context'; export default function ({ loadTestFile }: DeploymentAgnosticFtrProviderContext) { - describe('Serverless Search - Deployment-agnostic api integration tests', () => { + describe('Serverless Search - Deployment-agnostic api integration tests', function () { + this.tags(['esGate']); + // load new search and platform deployment-agnostic test here loadTestFile(require.resolve('../../apis/console')); loadTestFile(require.resolve('../../apis/core')); diff --git a/x-pack/test/api_integration/deployment_agnostic/configs/serverless/security.index.ts b/x-pack/test/api_integration/deployment_agnostic/configs/serverless/security.index.ts index 20046ab60c700..9e750ccf898f3 100644 --- a/x-pack/test/api_integration/deployment_agnostic/configs/serverless/security.index.ts +++ b/x-pack/test/api_integration/deployment_agnostic/configs/serverless/security.index.ts @@ -7,7 +7,9 @@ import { DeploymentAgnosticFtrProviderContext } from '../../ftr_provider_context'; export default function ({ loadTestFile }: DeploymentAgnosticFtrProviderContext) { - describe('Serverless Security - Deployment-agnostic api integration tests', () => { + describe('Serverless Security - Deployment-agnostic api integration tests', function () { + this.tags(['esGate']); + // load new security and platform deployment-agnostic test here loadTestFile(require.resolve('../../apis/console')); loadTestFile(require.resolve('../../apis/core')); diff --git a/x-pack/test/apm_api_integration/tests/entities/services/services_entities_detailed_statistics.spec.ts b/x-pack/test/apm_api_integration/tests/entities/services/services_entities_detailed_statistics.spec.ts deleted file mode 100644 index e1bf212c6e9c0..0000000000000 --- a/x-pack/test/apm_api_integration/tests/entities/services/services_entities_detailed_statistics.spec.ts +++ /dev/null @@ -1,58 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license 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 { APIClientRequestParamsOf } from '@kbn/apm-plugin/public/services/rest/create_call_apm_api'; -import { FtrProviderContext } from '../../../common/ftr_provider_context'; - -export default function ApiTest({ getService }: FtrProviderContext) { - const registry = getService('registry'); - - const apmApiClient = getService('apmApiClient'); - - const start = '2024-01-01T00:00:00.000Z'; - const end = '2024-01-01T00:59:59.999Z'; - - const serviceNames = ['my-service', 'synth-go']; - - async function getServiceEntitiesDetailedStats( - overrides?: Partial< - APIClientRequestParamsOf<'POST /internal/apm/entities/services/detailed_statistics'>['params']['query'] - > - ) { - const response = await apmApiClient.readUser({ - endpoint: `POST /internal/apm/entities/services/detailed_statistics`, - params: { - query: { - start, - end, - environment: 'ENVIRONMENT_ALL', - kuery: '', - ...overrides, - }, - body: { - serviceNames: JSON.stringify(serviceNames), - }, - }, - }); - - return response; - } - - registry.when( - 'Services entities detailed statistics when no data is generated', - { config: 'basic', archives: [] }, - () => { - describe('Service entities detailed', () => { - it('handles the empty state', async () => { - const response = await getServiceEntitiesDetailedStats(); - expect(response.status).to.be(200); - expect(response.body.currentPeriod).to.empty(); - }); - }); - } - ); -} diff --git a/x-pack/test/cloud_security_posture_api/utils.ts b/x-pack/test/cloud_security_posture_api/utils.ts index 6f0d86419a349..9f0805c2e85c1 100644 --- a/x-pack/test/cloud_security_posture_api/utils.ts +++ b/x-pack/test/cloud_security_posture_api/utils.ts @@ -23,7 +23,7 @@ export const waitForPluginInitialized = ({ }: { retry: RetryService; logger: ToolingLog; - supertest: Agent; + supertest: Pick; }): Promise => retry.try(async () => { logger.debug('Check CSP plugin is initialized'); @@ -44,13 +44,16 @@ export class EsIndexDataProvider { this.index = index; } - addBulk(docs: Array>, overrideTimestamp = true) { + async addBulk(docs: Array>, overrideTimestamp = true) { const operations = docs.flatMap((doc) => [ - { index: { _index: this.index } }, + { create: { _index: this.index } }, { ...doc, ...(overrideTimestamp ? { '@timestamp': new Date().toISOString() } : {}) }, ]); - return this.es.bulk({ refresh: 'wait_for', index: this.index, operations }); + const resp = await this.es.bulk({ refresh: 'wait_for', index: this.index, operations }); + expect(resp.errors).eql(false, `Error in bulk indexing: ${JSON.stringify(resp)}`); + + return resp; } async deleteAll() { diff --git a/x-pack/test/cloud_security_posture_functional/agentless/index.ts b/x-pack/test/cloud_security_posture_functional/agentless/index.ts new file mode 100644 index 0000000000000..02f10dc5cc348 --- /dev/null +++ b/x-pack/test/cloud_security_posture_functional/agentless/index.ts @@ -0,0 +1,16 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { FtrProviderContext } from '../ftr_provider_context'; + +// eslint-disable-next-line import/no-default-export +export default function ({ loadTestFile }: FtrProviderContext) { + describe('Cloud Security Posture', function () { + loadTestFile(require.resolve('./create_agent')); + loadTestFile(require.resolve('./security_posture')); + }); +} diff --git a/x-pack/test/cloud_security_posture_functional/agentless/security_posture.ts b/x-pack/test/cloud_security_posture_functional/agentless/security_posture.ts new file mode 100644 index 0000000000000..c7ee5ff8400e6 --- /dev/null +++ b/x-pack/test/cloud_security_posture_functional/agentless/security_posture.ts @@ -0,0 +1,87 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { CLOUD_CREDENTIALS_PACKAGE_VERSION } from '@kbn/cloud-security-posture-plugin/common/constants'; +import expect from '@kbn/expect'; +import type { FtrProviderContext } from '../ftr_provider_context'; +// eslint-disable-next-line import/no-default-export +export default function ({ getPageObjects, getService }: FtrProviderContext) { + const testSubjects = getService('testSubjects'); + const pageObjects = getPageObjects([ + 'common', + 'cspSecurity', + 'security', + 'header', + 'cisAddIntegration', + ]); + + const KSPM_RADIO_OPTION = 'policy-template-radio-button-kspm'; + const CSPM_RADIO_OPTION = 'policy-template-radio-button-cspm'; + const CNVM_RADIO_OPTION = 'policy-template-radio-button-vuln_mgmt'; + + const POLICY_NAME_FIELD = 'createAgentPolicyNameField'; + const SETUP_TECHNOLOGY_SELECTOR = 'setup-technology-selector-accordion'; + + describe('Agentless Security Posture Integration Options', function () { + let cisIntegration: typeof pageObjects.cisAddIntegration; + + before(async () => { + cisIntegration = pageObjects.cisAddIntegration; + }); + + after(async () => { + await pageObjects.cspSecurity.logout(); + }); + + it(`should show kspm without agentless option`, async () => { + await cisIntegration.navigateToAddIntegrationWithVersionPage( + CLOUD_CREDENTIALS_PACKAGE_VERSION + ); + + await cisIntegration.clickOptionButton(KSPM_RADIO_OPTION); + await pageObjects.header.waitUntilLoadingHasFinished(); + + const hasSetupTechnologySelector = await testSubjects.exists(SETUP_TECHNOLOGY_SELECTOR); + const hasAgentBased = await testSubjects.exists(POLICY_NAME_FIELD); + + expect(hasSetupTechnologySelector).to.be(false); + expect(hasAgentBased).to.be(true); + }); + + it(`should show cnvm without agentless option`, async () => { + // const integrationPolicyName = `cloud_security_posture-${new Date().toISOString()}`; + await cisIntegration.navigateToAddIntegrationWithVersionPage( + CLOUD_CREDENTIALS_PACKAGE_VERSION + ); + + await cisIntegration.clickOptionButton(CNVM_RADIO_OPTION); + await pageObjects.header.waitUntilLoadingHasFinished(); + + const hasSetupTechnologySelector = await testSubjects.exists(SETUP_TECHNOLOGY_SELECTOR); + const hasAgentBased = await testSubjects.exists(POLICY_NAME_FIELD); + + expect(hasSetupTechnologySelector).to.be(false); + expect(hasAgentBased).to.be(true); + }); + + it(`should show cspm with agentless option`, async () => { + // const integrationPolicyName = `cloud_security_posture-${new Date().toISOString()}`; + await cisIntegration.navigateToAddIntegrationWithVersionPage( + CLOUD_CREDENTIALS_PACKAGE_VERSION + ); + + await cisIntegration.clickOptionButton(CSPM_RADIO_OPTION); + await pageObjects.header.waitUntilLoadingHasFinished(); + + const hasSetupTechnologySelector = await testSubjects.exists(SETUP_TECHNOLOGY_SELECTOR); + const hasAgentBased = await testSubjects.exists(POLICY_NAME_FIELD); + + expect(hasSetupTechnologySelector).to.be(true); + expect(hasAgentBased).to.be(true); + }); + }); +} diff --git a/x-pack/test/cloud_security_posture_functional/config.agentless.ts b/x-pack/test/cloud_security_posture_functional/config.agentless.ts index 341ef6a9905b7..498de6d888223 100644 --- a/x-pack/test/cloud_security_posture_functional/config.agentless.ts +++ b/x-pack/test/cloud_security_posture_functional/config.agentless.ts @@ -7,6 +7,7 @@ import type { FtrConfigProviderContext } from '@kbn/test'; import { CA_CERT_PATH, KBN_CERT_PATH, KBN_KEY_PATH } from '@kbn/dev-utils'; +import { CLOUD_CREDENTIALS_PACKAGE_VERSION } from '@kbn/cloud-security-posture-plugin/common/constants'; import { pageObjects } from './page_objects'; export default async function ({ readConfigFile }: FtrConfigProviderContext) { @@ -30,9 +31,11 @@ export default async function ({ readConfigFile }: FtrConfigProviderContext) { `--xpack.fleet.agentless.api.tls.key=${KBN_KEY_PATH}`, `--xpack.fleet.agentless.api.tls.ca=${CA_CERT_PATH}`, `--xpack.cloud.id=something-anything`, + `--xpack.fleet.packages.0.name=cloud_security_posture`, + `--xpack.fleet.packages.0.version=${CLOUD_CREDENTIALS_PACKAGE_VERSION}`, ], }, // load tests in the index file - testFiles: [require.resolve('./agentless/create_agent.ts')], + testFiles: [require.resolve('./agentless')], }; } diff --git a/x-pack/test/cloud_security_posture_functional/page_objects/add_cis_integration_form_page.ts b/x-pack/test/cloud_security_posture_functional/page_objects/add_cis_integration_form_page.ts index e3ef420055196..563507d705583 100644 --- a/x-pack/test/cloud_security_posture_functional/page_objects/add_cis_integration_form_page.ts +++ b/x-pack/test/cloud_security_posture_functional/page_objects/add_cis_integration_form_page.ts @@ -154,6 +154,27 @@ export function AddCisIntegrationFormPageProvider({ await PageObjects.header.waitUntilLoadingHasFinished(); }; + const navigateToAddIntegrationWithVersionPage = async ( + packageVersion: string, + space?: string + ) => { + const options = space + ? { + basePath: `/s/${space}`, + shouldUseHashForSubUrl: false, + } + : { + shouldUseHashForSubUrl: false, + }; + + await PageObjects.common.navigateToUrl( + 'fleet', + `integrations/cloud_security_posture-${packageVersion}/add-integration`, + options + ); + await PageObjects.header.waitUntilLoadingHasFinished(); + }; + const navigateToAddIntegrationCspmWithVersionPage = async ( packageVersion: string, space?: string @@ -505,6 +526,7 @@ export function AddCisIntegrationFormPageProvider({ cisAzure, cisAws, cisGcp, + navigateToAddIntegrationWithVersionPage, navigateToAddIntegrationCspmPage, navigateToAddIntegrationCspmWithVersionPage, navigateToAddIntegrationCnvmPage, diff --git a/x-pack/test/cloud_security_posture_functional/page_objects/findings_page.ts b/x-pack/test/cloud_security_posture_functional/page_objects/findings_page.ts index 8e9483ce97b33..f58f798a96df4 100644 --- a/x-pack/test/cloud_security_posture_functional/page_objects/findings_page.ts +++ b/x-pack/test/cloud_security_posture_functional/page_objects/findings_page.ts @@ -366,8 +366,7 @@ export function FindingsPageProvider({ getService, getPageObjects }: FtrProvider }); const isLatestFindingsTableThere = async () => { const table = await testSubjects.findAll('docTable'); - const trueOrFalse = table.length > 0 ? true : false; - return trueOrFalse; + return table.length > 0; }; const getUnprivilegedPrompt = async () => { diff --git a/x-pack/test/cloud_security_posture_functional/pages/findings_old_data.ts b/x-pack/test/cloud_security_posture_functional/pages/findings_old_data.ts index 9586387c028ea..2811d2427108b 100644 --- a/x-pack/test/cloud_security_posture_functional/pages/findings_old_data.ts +++ b/x-pack/test/cloud_security_posture_functional/pages/findings_old_data.ts @@ -8,16 +8,38 @@ import expect from '@kbn/expect'; import Chance from 'chance'; import type { FtrProviderContext } from '../ftr_provider_context'; +import { vulnerabilitiesLatestMock } from '../mocks/vulnerabilities_latest_mock'; // eslint-disable-next-line import/no-default-export -export default function ({ getPageObjects }: FtrProviderContext) { +export default function ({ getPageObjects, getService }: FtrProviderContext) { + const retry = getService('retry'); const pageObjects = getPageObjects(['common', 'findings', 'header']); const chance = new Chance(); - const hoursToMillisecond = (hours: number) => hours * 60 * 60 * 1000; + const daysToMillisecond = (days: number) => days * 24 * 60 * 60 * 1000; + const RETENTION = 90; const dataOldKspm = [ { - '@timestamp': (Date.now() - hoursToMillisecond(27)).toString(), + '@timestamp': (Date.now() - daysToMillisecond(RETENTION + 1)).toString(), + resource: { id: chance.guid(), name: `kubelet`, sub_type: 'lower case sub type' }, + result: { evaluation: chance.integer() % 2 === 0 ? 'passed' : 'failed' }, + rule: { + name: 'Upper case rule name', + section: 'Upper case section', + benchmark: { + id: 'cis_k8s', + posture_type: 'kspm', + name: 'CIS Kubernetes V1.23', + version: 'v1.0.0', + }, + type: 'process', + }, + cluster_id: 'Upper case cluster id', + }, + ]; + const dataWithinRetentionKspm = [ + { + '@timestamp': (Date.now() - daysToMillisecond(RETENTION - 1)).toString(), resource: { id: chance.guid(), name: `kubelet`, sub_type: 'lower case sub type' }, result: { evaluation: chance.integer() % 2 === 0 ? 'passed' : 'failed' }, rule: { @@ -37,7 +59,26 @@ export default function ({ getPageObjects }: FtrProviderContext) { const dataOldCspm = [ { - '@timestamp': (Date.now() - hoursToMillisecond(27)).toString(), + '@timestamp': (Date.now() - daysToMillisecond(RETENTION + 1)).toString(), + resource: { id: chance.guid(), name: `kubelet`, sub_type: 'lower case sub type' }, + result: { evaluation: chance.integer() % 2 === 0 ? 'passed' : 'failed' }, + rule: { + name: 'Upper case rule name', + section: 'Upper case section', + benchmark: { + id: 'cis_aws', + posture_type: 'cspm', + name: 'CIS AWS V1.23', + version: 'v1.0.0', + }, + type: 'process', + }, + cluster_id: 'Upper case cluster id', + }, + ]; + const dataWithinRetentionCspm = [ + { + '@timestamp': (Date.now() - daysToMillisecond(RETENTION - 1)).toString(), resource: { id: chance.guid(), name: `kubelet`, sub_type: 'lower case sub type' }, result: { evaluation: chance.integer() % 2 === 0 ? 'passed' : 'failed' }, rule: { @@ -55,25 +96,41 @@ export default function ({ getPageObjects }: FtrProviderContext) { }, ]; + const dataOldCnvm = [ + { + ...vulnerabilitiesLatestMock[0], + '@timestamp': (Date.now() - daysToMillisecond(RETENTION + 1)).toString(), + }, + ]; + const dataWithinRetentionCnvm = [ + { + ...vulnerabilitiesLatestMock[0], + '@timestamp': (Date.now() - daysToMillisecond(RETENTION - 1)).toString(), + }, + ]; + describe('Old Data', function () { this.tags(['cloud_security_posture_findings']); let findings: typeof pageObjects.findings; + let latestFindingsTable: typeof findings.latestFindingsTable; + let latestVulnerabilitiesTable: typeof findings.latestVulnerabilitiesTable; before(async () => { findings = pageObjects.findings; + latestFindingsTable = findings.latestFindingsTable; + latestVulnerabilitiesTable = findings.latestVulnerabilitiesTable; // Before we start any test we must wait for cloud_security_posture plugin to complete its initialization await findings.waitForPluginInitialized(); }); - after(async () => { + afterEach(async () => { await findings.index.remove(); + await findings.vulnerabilitiesIndex.remove(); }); describe('Findings page with old data', () => { it('returns no Findings KSPM', async () => { - // Prepare mocked findings - await findings.index.remove(); await findings.index.add(dataOldKspm); await findings.navigateToLatestFindingsPage(); @@ -81,14 +138,50 @@ export default function ({ getPageObjects }: FtrProviderContext) { expect(await findings.isLatestFindingsTableThere()).to.be(false); }); it('returns no Findings CSPM', async () => { - // Prepare mocked findings - await findings.index.remove(); await findings.index.add(dataOldCspm); await findings.navigateToLatestFindingsPage(); await pageObjects.header.waitUntilLoadingHasFinished(); expect(await findings.isLatestFindingsTableThere()).to.be(false); }); + it('returns no Findings CNVM', async () => { + await findings.vulnerabilitiesIndex.add(dataOldCnvm); + + await findings.navigateToLatestVulnerabilitiesPage(); + await pageObjects.header.waitUntilLoadingHasFinished(); + expect(await findings.isLatestFindingsTableThere()).to.be(false); + }); + it('returns data grid with only data within retention KSPM', async () => { + await findings.index.add([...dataOldKspm, ...dataWithinRetentionKspm]); + + await findings.navigateToLatestFindingsPage(); + await retry.waitFor( + 'Findings table to be loaded', + async () => (await latestFindingsTable.getRowsCount()) === dataWithinRetentionKspm.length + ); + await pageObjects.header.waitUntilLoadingHasFinished(); + }); + it('returns data grid with only data within retention CSPM', async () => { + await findings.index.add([...dataOldCspm, ...dataWithinRetentionCspm]); + + await findings.navigateToLatestFindingsPage(); + await retry.waitFor( + 'Findings table to be loaded', + async () => (await latestFindingsTable.getRowsCount()) === dataWithinRetentionCspm.length + ); + await pageObjects.header.waitUntilLoadingHasFinished(); + }); + it('returns data grid with only data within retention CSPM', async () => { + await findings.vulnerabilitiesIndex.add([...dataOldCnvm, ...dataWithinRetentionCnvm]); + + await findings.navigateToLatestVulnerabilitiesPage(); + await retry.waitFor( + 'Findings table to be loaded', + async () => + (await latestVulnerabilitiesTable.getRowsCount()) === dataWithinRetentionCnvm.length + ); + await pageObjects.header.waitUntilLoadingHasFinished(); + }); }); }); } diff --git a/x-pack/test/encrypted_saved_objects_api_integration/config.ts b/x-pack/test/encrypted_saved_objects_api_integration/config.ts index 0b2585a94e954..433cbb9e36151 100644 --- a/x-pack/test/encrypted_saved_objects_api_integration/config.ts +++ b/x-pack/test/encrypted_saved_objects_api_integration/config.ts @@ -6,7 +6,9 @@ */ import path from 'path'; -import { FtrConfigProviderContext } from '@kbn/test'; + +import type { FtrConfigProviderContext } from '@kbn/test'; + import { services } from './services'; export default async function ({ readConfigFile }: FtrConfigProviderContext) { diff --git a/x-pack/test/encrypted_saved_objects_api_integration/ftr_provider_context.d.ts b/x-pack/test/encrypted_saved_objects_api_integration/ftr_provider_context.d.ts index aa56557c09df8..b05bbc8f6318d 100644 --- a/x-pack/test/encrypted_saved_objects_api_integration/ftr_provider_context.d.ts +++ b/x-pack/test/encrypted_saved_objects_api_integration/ftr_provider_context.d.ts @@ -5,8 +5,8 @@ * 2.0. */ -import { GenericFtrProviderContext } from '@kbn/test'; +import type { GenericFtrProviderContext } from '@kbn/test'; -import { services } from './services'; +import type { services } from './services'; export type FtrProviderContext = GenericFtrProviderContext; diff --git a/x-pack/test/encrypted_saved_objects_api_integration/plugins/api_consumer_plugin/server/hidden_saved_object_routes.ts b/x-pack/test/encrypted_saved_objects_api_integration/plugins/api_consumer_plugin/server/hidden_saved_object_routes.ts index 363d463139e4c..d0edb19e6a639 100644 --- a/x-pack/test/encrypted_saved_objects_api_integration/plugins/api_consumer_plugin/server/hidden_saved_object_routes.ts +++ b/x-pack/test/encrypted_saved_objects_api_integration/plugins/api_consumer_plugin/server/hidden_saved_object_routes.ts @@ -6,8 +6,9 @@ */ import { schema } from '@kbn/config-schema'; -import { IRouter, CoreSetup, SavedObject } from '@kbn/core/server'; -import { PluginsSetup, PluginsStart } from '.'; +import type { CoreSetup, IRouter, SavedObject } from '@kbn/core/server'; + +import type { PluginsSetup, PluginsStart } from '.'; export function registerHiddenSORoutes( router: IRouter, diff --git a/x-pack/test/encrypted_saved_objects_api_integration/plugins/api_consumer_plugin/server/index.ts b/x-pack/test/encrypted_saved_objects_api_integration/plugins/api_consumer_plugin/server/index.ts index f73d88fc79746..c7946b2e68131 100644 --- a/x-pack/test/encrypted_saved_objects_api_integration/plugins/api_consumer_plugin/server/index.ts +++ b/x-pack/test/encrypted_saved_objects_api_integration/plugins/api_consumer_plugin/server/index.ts @@ -5,20 +5,21 @@ * 2.0. */ -import { deepFreeze } from '@kbn/std'; -import { +import { schema } from '@kbn/config-schema'; +import type { CoreSetup, PluginInitializer, + SavedObject, SavedObjectsNamespaceType, SavedObjectUnsanitizedDoc, - SavedObject, } from '@kbn/core/server'; -import { schema } from '@kbn/config-schema'; -import { +import type { EncryptedSavedObjectsPluginSetup, EncryptedSavedObjectsPluginStart, } from '@kbn/encrypted-saved-objects-plugin/server'; -import { SpacesPluginSetup } from '@kbn/spaces-plugin/server'; +import type { SpacesPluginSetup } from '@kbn/spaces-plugin/server'; +import { deepFreeze } from '@kbn/std'; + import { registerHiddenSORoutes } from './hidden_saved_object_routes'; const SAVED_OBJECT_WITH_SECRET_TYPE = 'saved-object-with-secret'; diff --git a/x-pack/test/encrypted_saved_objects_api_integration/tests/encrypted_saved_objects_aad_include_list.ts b/x-pack/test/encrypted_saved_objects_api_integration/tests/encrypted_saved_objects_aad_include_list.ts index fd6c0305f88ce..ae80cae697db9 100644 --- a/x-pack/test/encrypted_saved_objects_api_integration/tests/encrypted_saved_objects_aad_include_list.ts +++ b/x-pack/test/encrypted_saved_objects_api_integration/tests/encrypted_saved_objects_aad_include_list.ts @@ -7,7 +7,8 @@ import { MAIN_SAVED_OBJECT_INDEX } from '@kbn/core-saved-objects-server'; import expect from '@kbn/expect'; -import { FtrProviderContext } from '../ftr_provider_context'; + +import type { FtrProviderContext } from '../ftr_provider_context'; export default function ({ getService }: FtrProviderContext) { const supertest = getService('supertest'); diff --git a/x-pack/test/encrypted_saved_objects_api_integration/tests/encrypted_saved_objects_api.ts b/x-pack/test/encrypted_saved_objects_api_integration/tests/encrypted_saved_objects_api.ts index 62a9545afa77d..4687a01858260 100644 --- a/x-pack/test/encrypted_saved_objects_api_integration/tests/encrypted_saved_objects_api.ts +++ b/x-pack/test/encrypted_saved_objects_api_integration/tests/encrypted_saved_objects_api.ts @@ -5,13 +5,12 @@ * 2.0. */ -import expect from '@kbn/expect'; import type { SavedObject } from '@kbn/core/server'; import { MAIN_SAVED_OBJECT_INDEX } from '@kbn/core-saved-objects-server'; -import { - descriptorToArray, - SavedObjectDescriptor, -} from '@kbn/encrypted-saved-objects-plugin/server/crypto'; +import type { SavedObjectDescriptor } from '@kbn/encrypted-saved-objects-plugin/server/crypto'; +import { descriptorToArray } from '@kbn/encrypted-saved-objects-plugin/server/crypto'; +import expect from '@kbn/expect'; + import type { FtrProviderContext } from '../ftr_provider_context'; export default function ({ getService }: FtrProviderContext) { diff --git a/x-pack/test/encrypted_saved_objects_api_integration/tests/encrypted_saved_objects_decryption.ts b/x-pack/test/encrypted_saved_objects_api_integration/tests/encrypted_saved_objects_decryption.ts index 8b0471cd34a9a..99428c2f0797f 100644 --- a/x-pack/test/encrypted_saved_objects_api_integration/tests/encrypted_saved_objects_decryption.ts +++ b/x-pack/test/encrypted_saved_objects_api_integration/tests/encrypted_saved_objects_decryption.ts @@ -7,7 +7,8 @@ import { MAIN_SAVED_OBJECT_INDEX } from '@kbn/core-saved-objects-server'; import expect from '@kbn/expect'; -import { FtrProviderContext } from '../ftr_provider_context'; + +import type { FtrProviderContext } from '../ftr_provider_context'; export default function ({ getService }: FtrProviderContext) { const supertest = getService('supertest'); 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 c3188d0f5d0c3..1b14ec04811cd 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 @@ -5,7 +5,7 @@ * 2.0. */ -import { FtrProviderContext } from '../ftr_provider_context'; +import type { FtrProviderContext } from '../ftr_provider_context'; export default function ({ loadTestFile }: FtrProviderContext) { describe('encryptedSavedObjects', function encryptedSavedObjectsSuite() { diff --git a/x-pack/test/fleet_cypress/cli_config.space_awareness.ts b/x-pack/test/fleet_cypress/cli_config.space_awareness.ts new file mode 100644 index 0000000000000..2fbaca2da9eca --- /dev/null +++ b/x-pack/test/fleet_cypress/cli_config.space_awareness.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 { FtrConfigProviderContext } from '@kbn/test'; + +import { FleetCypressCliTestRunner } from './runner'; + +export default async function ({ readConfigFile }: FtrConfigProviderContext) { + const cypressConfig = await readConfigFile(require.resolve('./config.space_awareness.ts')); + return { + ...cypressConfig.getAll(), + + testRunner: FleetCypressCliTestRunner, + }; +} diff --git a/x-pack/test/fleet_cypress/config.space_awareness.ts b/x-pack/test/fleet_cypress/config.space_awareness.ts new file mode 100644 index 0000000000000..eeee016b0c4d3 --- /dev/null +++ b/x-pack/test/fleet_cypress/config.space_awareness.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; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { FtrConfigProviderContext, getKibanaCliLoggers } from '@kbn/test'; +import { CA_CERT_PATH } from '@kbn/dev-utils'; + +export default async function ({ readConfigFile }: FtrConfigProviderContext) { + const kibanaCommonTestsConfig = await readConfigFile( + require.resolve('@kbn/test-suites-src/common/config') + ); + const xpackFunctionalTestsConfig = await readConfigFile( + require.resolve('../functional/config.base.js') + ); + + return { + ...kibanaCommonTestsConfig.getAll(), + + esTestCluster: { + ...xpackFunctionalTestsConfig.get('esTestCluster'), + serverArgs: [ + ...xpackFunctionalTestsConfig.get('esTestCluster.serverArgs'), + // define custom es server here + // API Keys is enabled at the top level + 'xpack.security.enabled=true', + 'http.host=0.0.0.0', + ], + }, + + kbnTestServer: { + ...xpackFunctionalTestsConfig.get('kbnTestServer'), + serverArgs: [ + ...xpackFunctionalTestsConfig.get('kbnTestServer.serverArgs'), + '--csp.warnLegacyBrowsers=false', + '--csp.strict=false', + // define custom kibana server args here + `--elasticsearch.ssl.certificateAuthorities=${CA_CERT_PATH}`, + + // add feature flags here + `--xpack.fleet.enableExperimental=${JSON.stringify([ + 'agentTamperProtectionEnabled', + 'subfeaturePrivileges', + 'useSpaceAwareness', + ])}`, + + `--logging.loggers=${JSON.stringify([ + ...getKibanaCliLoggers(xpackFunctionalTestsConfig.get('kbnTestServer.serverArgs')), + + // Enable debug fleet logs by default + { + name: 'plugins.fleet', + level: 'debug', + appenders: ['default'], + }, + ])}`, + ], + }, + }; +} diff --git a/x-pack/test/functional/apps/data_views/feature_controls/security.ts b/x-pack/test/functional/apps/data_views/feature_controls/security.ts index 1cc62baf0abba..34317932a6b21 100644 --- a/x-pack/test/functional/apps/data_views/feature_controls/security.ts +++ b/x-pack/test/functional/apps/data_views/feature_controls/security.ts @@ -131,10 +131,12 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) { expect(navLinks).to.eql(['Stack Management']); }); - it(`index pattern listing doesn't show create button`, async () => { + it(`index pattern listing shows disabled create button`, async () => { await settings.clickKibanaIndexPatterns(); await testSubjects.existOrFail('noDataViewsPrompt'); - await testSubjects.missingOrFail('createDataViewButton'); + const createDataViewButton = await testSubjects.find('createDataViewButton'); + const isDisabled = await createDataViewButton.getAttribute('disabled'); + expect(isDisabled).to.be('true'); }); it(`shows read-only badge`, async () => { diff --git a/x-pack/test/functional/apps/dev_tools/searchprofiler_editor.ts b/x-pack/test/functional/apps/dev_tools/searchprofiler_editor.ts index 174f3d4527178..87c36de62bba6 100644 --- a/x-pack/test/functional/apps/dev_tools/searchprofiler_editor.ts +++ b/x-pack/test/functional/apps/dev_tools/searchprofiler_editor.ts @@ -67,7 +67,7 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) { `parser errors to match expectation: HAS ${expectation ? 'ERRORS' : 'NO ERRORS'}`, async () => { const actual = await PageObjects.searchProfiler.editorHasParseErrors(); - return expectation === actual; + return expectation === actual?.length > 0; } ); } diff --git a/x-pack/test/functional/apps/infra/logs/log_entry_categories_tab.ts b/x-pack/test/functional/apps/infra/logs/log_entry_categories_tab.ts index 33396497fc83c..0d4a5440ebd58 100644 --- a/x-pack/test/functional/apps/infra/logs/log_entry_categories_tab.ts +++ b/x-pack/test/functional/apps/infra/logs/log_entry_categories_tab.ts @@ -9,14 +9,54 @@ import expect from '@kbn/expect'; import { FtrProviderContext } from '../../../ftr_provider_context'; -export default ({ getService }: FtrProviderContext) => { +export default ({ getPageObjects, getService }: FtrProviderContext) => { + const PageObjects = getPageObjects(['security']); const esArchiver = getService('esArchiver'); const logsUi = getService('logsUi'); const retry = getService('retry'); + const security = getService('security'); describe('Log Entry Categories Tab', function () { this.tags('includeFirefox'); + const loginWithMLPrivileges = async (privileges: Record) => { + await security.role.create('global_logs_role', { + elasticsearch: { + cluster: ['all'], + indices: [{ names: ['*'], privileges: ['read', 'view_index_metadata'] }], + }, + kibana: [ + { + feature: { + logs: ['read'], + ...privileges, + }, + spaces: ['*'], + }, + ], + }); + + await security.user.create('global_logs_read_user', { + password: 'global_logs_read_user-password', + roles: ['global_logs_role'], + full_name: 'logs test user', + }); + + await PageObjects.security.forceLogout(); + + await PageObjects.security.login('global_logs_read_user', 'global_logs_read_user-password', { + expectSpaceSelector: false, + }); + }; + + const logoutAndDeleteUser = async () => { + await PageObjects.security.forceLogout(); + await Promise.all([ + security.role.delete('global_logs_role'), + security.user.delete('global_logs_read_user'), + ]); + }; + describe('with a trial license', () => { it('Shows no data page when indices do not exist', async () => { await logsUi.logEntryCategoriesPage.navigateTo(); @@ -26,14 +66,42 @@ export default ({ getService }: FtrProviderContext) => { }); }); - it('shows setup page when indices exist', async () => { - await esArchiver.load('x-pack/test/functional/es_archives/infra/simple_logs'); - await logsUi.logEntryCategoriesPage.navigateTo(); + describe('when indices exists', () => { + before(async () => { + await esArchiver.load('x-pack/test/functional/es_archives/infra/metrics_and_logs'); + }); - await retry.try(async () => { - expect(await logsUi.logEntryCategoriesPage.getSetupScreen()).to.be.ok(); + after(async () => { + await esArchiver.unload('x-pack/test/functional/es_archives/infra/metrics_and_logs'); + }); + + it('shows setup page when indices exist', async () => { + await logsUi.logEntryCategoriesPage.navigateTo(); + + await retry.try(async () => { + expect(await logsUi.logEntryCategoriesPage.getSetupScreen()).to.be.ok(); + }); + }); + + it('shows required ml read privileges prompt when the user has not any ml privileges', async () => { + await loginWithMLPrivileges({}); + await logsUi.logEntryCategoriesPage.navigateTo(); + + await retry.try(async () => { + expect(await logsUi.logEntryCategoriesPage.getNoMlReadPrivilegesPrompt()).to.be.ok(); + }); + await logoutAndDeleteUser(); + }); + + it('shows required ml all privileges prompt when the user has only ml read privileges', async () => { + await loginWithMLPrivileges({ ml: ['read'] }); + await logsUi.logEntryCategoriesPage.navigateTo(); + + await retry.try(async () => { + expect(await logsUi.logEntryCategoriesPage.getNoMlAllPrivilegesPrompt()).to.be.ok(); + }); + await logoutAndDeleteUser(); }); - await esArchiver.unload('x-pack/test/functional/es_archives/infra/simple_logs'); }); }); }); diff --git a/x-pack/test/functional/apps/infra/logs/log_entry_rate_tab.ts b/x-pack/test/functional/apps/infra/logs/log_entry_rate_tab.ts index b2b4b5bcfc0be..35aa6ec6ca4ae 100644 --- a/x-pack/test/functional/apps/infra/logs/log_entry_rate_tab.ts +++ b/x-pack/test/functional/apps/infra/logs/log_entry_rate_tab.ts @@ -9,16 +9,56 @@ import expect from '@kbn/expect'; import { FtrProviderContext } from '../../../ftr_provider_context'; -export default ({ getService }: FtrProviderContext) => { +export default ({ getPageObjects, getService }: FtrProviderContext) => { + const PageObjects = getPageObjects(['security']); const logsUi = getService('logsUi'); const retry = getService('retry'); const esArchiver = getService('esArchiver'); + const security = getService('security'); describe('Log Entry Rate Tab', function () { this.tags('includeFirefox'); + const loginWithMLPrivileges = async (privileges: Record) => { + await security.role.create('global_logs_role', { + elasticsearch: { + cluster: ['all'], + indices: [{ names: ['*'], privileges: ['read', 'view_index_metadata'] }], + }, + kibana: [ + { + feature: { + logs: ['read'], + ...privileges, + }, + spaces: ['*'], + }, + ], + }); + + await security.user.create('global_logs_read_user', { + password: 'global_logs_read_user-password', + roles: ['global_logs_role'], + full_name: 'logs test user', + }); + + await PageObjects.security.forceLogout(); + + await PageObjects.security.login('global_logs_read_user', 'global_logs_read_user-password', { + expectSpaceSelector: false, + }); + }; + + const logoutAndDeleteUser = async () => { + await PageObjects.security.forceLogout(); + await Promise.all([ + security.role.delete('global_logs_role'), + security.user.delete('global_logs_read_user'), + ]); + }; + describe('with a trial license', () => { - it('Shows no data page when indices do not exist', async () => { + it('shows no data page when indices do not exist', async () => { await logsUi.logEntryRatePage.navigateTo(); await retry.try(async () => { @@ -26,14 +66,42 @@ export default ({ getService }: FtrProviderContext) => { }); }); - it('shows setup page when indices exist', async () => { - await esArchiver.load('x-pack/test/functional/es_archives/infra/simple_logs'); - await logsUi.logEntryRatePage.navigateTo(); + describe('when indices exists', () => { + before(async () => { + await esArchiver.load('x-pack/test/functional/es_archives/infra/metrics_and_logs'); + }); - await retry.try(async () => { - expect(await logsUi.logEntryRatePage.getSetupScreen()).to.be.ok(); + after(async () => { + await esArchiver.unload('x-pack/test/functional/es_archives/infra/metrics_and_logs'); + }); + + it('shows setup page when indices exist', async () => { + await logsUi.logEntryRatePage.navigateTo(); + + await retry.try(async () => { + expect(await logsUi.logEntryRatePage.getSetupScreen()).to.be.ok(); + }); + }); + + it('shows required ml read privileges prompt when the user has not any ml privileges', async () => { + await loginWithMLPrivileges({}); + await logsUi.logEntryRatePage.navigateTo(); + + await retry.try(async () => { + expect(await logsUi.logEntryRatePage.getNoMlReadPrivilegesPrompt()).to.be.ok(); + }); + await logoutAndDeleteUser(); + }); + + it('shows required ml all privileges prompt when the user has only ml read privileges', async () => { + await loginWithMLPrivileges({ ml: ['read'] }); + await logsUi.logEntryRatePage.navigateTo(); + + await retry.try(async () => { + expect(await logsUi.logEntryRatePage.getNoMlAllPrivilegesPrompt()).to.be.ok(); + }); + await logoutAndDeleteUser(); }); - await esArchiver.unload('x-pack/test/functional/es_archives/infra/simple_logs'); }); }); }); diff --git a/x-pack/test/functional/apps/infra/logs/log_stream.ts b/x-pack/test/functional/apps/infra/logs/log_stream.ts index 8592287477826..16dcc038f7aab 100644 --- a/x-pack/test/functional/apps/infra/logs/log_stream.ts +++ b/x-pack/test/functional/apps/infra/logs/log_stream.ts @@ -6,6 +6,7 @@ */ import expect from '@kbn/expect'; +import { OBSERVABILITY_ENABLE_LOGS_STREAM } from '@kbn/management-settings-ids'; import { URL } from 'url'; import { FtrProviderContext } from '../../../ftr_provider_context'; @@ -16,17 +17,20 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => { const retry = getService('retry'); const browser = getService('browser'); const esArchiver = getService('esArchiver'); + const kibanaServer = getService('kibanaServer'); describe('Log stream', function () { describe('Legacy URL handling', () => { describe('Correctly handles legacy versions of logFilter', () => { before(async () => { await esArchiver.load('x-pack/test/functional/es_archives/infra/8.0.0/logs_and_metrics'); + await kibanaServer.uiSettings.update({ [OBSERVABILITY_ENABLE_LOGS_STREAM]: true }); }); after(async () => { await esArchiver.unload( 'x-pack/test/functional/es_archives/infra/8.0.0/logs_and_metrics' ); + await kibanaServer.uiSettings.update({ [OBSERVABILITY_ENABLE_LOGS_STREAM]: false }); }); it('Expression and kind', async () => { const location = { diff --git a/x-pack/test/functional/apps/infra/logs/log_stream_date_nano.ts b/x-pack/test/functional/apps/infra/logs/log_stream_date_nano.ts index ed1f85248b303..141d1bc38c3d3 100644 --- a/x-pack/test/functional/apps/infra/logs/log_stream_date_nano.ts +++ b/x-pack/test/functional/apps/infra/logs/log_stream_date_nano.ts @@ -6,6 +6,7 @@ */ import expect from '@kbn/expect'; +import { OBSERVABILITY_ENABLE_LOGS_STREAM } from '@kbn/management-settings-ids'; import { FtrProviderContext } from '../../../ftr_provider_context'; import { DATES } from '../constants'; @@ -14,6 +15,7 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => { const esArchiver = getService('esArchiver'); const logsUi = getService('logsUi'); const find = getService('find'); + const kibanaServer = getService('kibanaServer'); const logFilter = { timeRange: { from: DATES.metricsAndLogs.stream.startWithData, @@ -24,9 +26,11 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => { describe('Log stream supports nano precision', function () { before(async () => { await esArchiver.load('x-pack/test/functional/es_archives/infra/logs_with_nano_date'); + await kibanaServer.uiSettings.update({ [OBSERVABILITY_ENABLE_LOGS_STREAM]: true }); }); after(async () => { await esArchiver.unload('x-pack/test/functional/es_archives/infra/logs_with_nano_date'); + await kibanaServer.uiSettings.update({ [OBSERVABILITY_ENABLE_LOGS_STREAM]: false }); }); it('should display logs entries containing date_nano timestamps properly ', async () => { diff --git a/x-pack/test/functional/apps/infra/logs/logs_source_configuration.ts b/x-pack/test/functional/apps/infra/logs/logs_source_configuration.ts index 4fdb4687faf6d..84158051021c3 100644 --- a/x-pack/test/functional/apps/infra/logs/logs_source_configuration.ts +++ b/x-pack/test/functional/apps/infra/logs/logs_source_configuration.ts @@ -10,6 +10,7 @@ import { ELASTIC_HTTP_VERSION_HEADER, X_ELASTIC_INTERNAL_ORIGIN_REQUEST, } from '@kbn/core-http-common'; +import { OBSERVABILITY_ENABLE_LOGS_STREAM } from '@kbn/management-settings-ids'; import { DATES } from '../constants'; import { FtrProviderContext } from '../../../ftr_provider_context'; @@ -31,9 +32,11 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => { describe('Logs Source Configuration', function () { before(async () => { await kibanaServer.savedObjects.cleanStandardList(); + await kibanaServer.uiSettings.update({ [OBSERVABILITY_ENABLE_LOGS_STREAM]: true }); }); after(async () => { await kibanaServer.savedObjects.cleanStandardList(); + await kibanaServer.uiSettings.update({ [OBSERVABILITY_ENABLE_LOGS_STREAM]: false }); }); describe('Allows indices configuration', () => { diff --git a/x-pack/test/functional/apps/infra/page_not_found.ts b/x-pack/test/functional/apps/infra/page_not_found.ts index 479d9979b918a..eb1fc77b4f9f9 100644 --- a/x-pack/test/functional/apps/infra/page_not_found.ts +++ b/x-pack/test/functional/apps/infra/page_not_found.ts @@ -6,6 +6,7 @@ */ import expect from '@kbn/expect'; +import { OBSERVABILITY_ENABLE_LOGS_STREAM } from '@kbn/management-settings-ids'; import { FtrProviderContext } from '../../ftr_provider_context'; const logsPages = ['logs/stream', 'logs/anomalies', 'logs/log-categories', 'logs/settings']; @@ -19,14 +20,22 @@ const metricsPages = [ ]; export default ({ getPageObjects, getService }: FtrProviderContext) => { - const find = getService('find'); const pageObjects = getPageObjects(['common', 'infraHome']); + const find = getService('find'); + const kibanaServer = getService('kibanaServer'); const testSubjects = getService('testSubjects'); describe('Infra Not Found page', function () { this.tags('includeFirefox'); describe('Logs', () => { + before(async () => { + await kibanaServer.uiSettings.update({ [OBSERVABILITY_ENABLE_LOGS_STREAM]: true }); + }); + after(async () => { + await kibanaServer.uiSettings.update({ [OBSERVABILITY_ENABLE_LOGS_STREAM]: false }); + }); + it('should render the not found page when the route does not exist', async () => { await pageObjects.common.navigateToApp('logs/broken-link'); await testSubjects.existOrFail('infraNotFoundPage'); diff --git a/x-pack/test/functional/page_objects/search_profiler_page.ts b/x-pack/test/functional/page_objects/search_profiler_page.ts index a110bd16eeafe..151b9a613c356 100644 --- a/x-pack/test/functional/page_objects/search_profiler_page.ts +++ b/x-pack/test/functional/page_objects/search_profiler_page.ts @@ -11,7 +11,7 @@ import { FtrProviderContext } from '../ftr_provider_context'; export function SearchProfilerPageProvider({ getService }: FtrProviderContext) { const find = getService('find'); const testSubjects = getService('testSubjects'); - const aceEditor = getService('aceEditor'); + const monacoEditor = getService('monacoEditor'); const editorTestSubjectSelector = 'searchProfilerEditor'; return { @@ -19,10 +19,10 @@ export function SearchProfilerPageProvider({ getService }: FtrProviderContext) { return await testSubjects.exists(editorTestSubjectSelector); }, async setQuery(query: any) { - await aceEditor.setValue(editorTestSubjectSelector, JSON.stringify(query)); + await monacoEditor.setCodeEditorValue(JSON.stringify(query), 0); }, async getQuery() { - return JSON.parse(await aceEditor.getValue(editorTestSubjectSelector)); + return JSON.parse(await monacoEditor.getCodeEditorValue(0)); }, async setIndexName(indexName: string) { await testSubjects.setValue('indexName', indexName); @@ -36,6 +36,7 @@ export function SearchProfilerPageProvider({ getService }: FtrProviderContext) { }, async getProfileContent() { const profileTree = await find.byClassName('prfDevTool__main__profiletree'); + // const profileTree = await find.byClassName('prfDevTool__page'); return profileTree.getVisibleText(); }, getUrlWithIndexAndQuery({ indexName, query }: { indexName: string; query: any }) { @@ -43,7 +44,7 @@ export function SearchProfilerPageProvider({ getService }: FtrProviderContext) { return `/searchprofiler?index=${indexName}&load_from=${searchQueryURI}`; }, async editorHasParseErrors() { - return await aceEditor.hasParseErrors(editorTestSubjectSelector); + return await monacoEditor.getCurrentMarkers(editorTestSubjectSelector); }, async editorHasErrorNotification() { const notification = await testSubjects.find('noShardsNotification'); diff --git a/x-pack/test/functional/services/logs_ui/log_entry_categories.ts b/x-pack/test/functional/services/logs_ui/log_entry_categories.ts index 77098bd918ea6..d270b510bffbd 100644 --- a/x-pack/test/functional/services/logs_ui/log_entry_categories.ts +++ b/x-pack/test/functional/services/logs_ui/log_entry_categories.ts @@ -24,5 +24,13 @@ export function LogEntryCategoriesPageProvider({ getPageObjects, getService }: F async getSetupScreen(): Promise { return await testSubjects.find('logEntryCategoriesSetupPage'); }, + + getNoMlReadPrivilegesPrompt() { + return testSubjects.find('logsMissingMLReadPrivileges'); + }, + + getNoMlAllPrivilegesPrompt() { + return testSubjects.find('logsMissingMLAllPrivileges'); + }, }; } diff --git a/x-pack/test/functional/services/logs_ui/log_entry_rate.ts b/x-pack/test/functional/services/logs_ui/log_entry_rate.ts index f8a68f6c924e0..9b704db9eb021 100644 --- a/x-pack/test/functional/services/logs_ui/log_entry_rate.ts +++ b/x-pack/test/functional/services/logs_ui/log_entry_rate.ts @@ -29,6 +29,14 @@ export function LogEntryRatePageProvider({ getPageObjects, getService }: FtrProv return await testSubjects.find('noDataPage'); }, + getNoMlReadPrivilegesPrompt() { + return testSubjects.find('logsMissingMLReadPrivileges'); + }, + + getNoMlAllPrivilegesPrompt() { + return testSubjects.find('logsMissingMLAllPrivileges'); + }, + async startJobSetup() { await testSubjects.click('infraLogEntryRateSetupContentMlSetupButton'); }, diff --git a/x-pack/test/functional/services/ml/trained_models_table.ts b/x-pack/test/functional/services/ml/trained_models_table.ts index 990ca6c1ed37a..8818df749ccd4 100644 --- a/x-pack/test/functional/services/ml/trained_models_table.ts +++ b/x-pack/test/functional/services/ml/trained_models_table.ts @@ -52,17 +52,16 @@ export function TrainedModelsTableProvider( id: string; description: string; modelTypes: string[]; - createdAt: string; state: string; } = { id: $tr .findTestSubject('mlModelsTableColumnId') - .find('.euiTableCellContent') + .findTestSubject('mlModelsTableColumnIdValueId') .text() .trim(), description: $tr - .findTestSubject('mlModelsTableColumnDescription') - .find('.euiTableCellContent') + .findTestSubject('mlModelsTableColumnId') + .findTestSubject('mlModelsTableColumnIdValueDescription') .text() .trim(), modelTypes, @@ -71,11 +70,6 @@ export function TrainedModelsTableProvider( .find('.euiTableCellContent') .text() .trim(), - createdAt: $tr - .findTestSubject('mlModelsTableColumnCreatedAt') - .find('.euiTableCellContent') - .text() - .trim(), }; rows.push(rowObject); @@ -161,12 +155,6 @@ export function TrainedModelsTableProvider( expectedRow.modelTypes )}' (got '${JSON.stringify(modelRow.modelTypes)}')` ); - // 'Created at' will be different on each run, - // so we will just assert that the value is in the expected timestamp format. - expect(modelRow.createdAt).to.match( - /^\w{3}\s\d+,\s\d{4}\s@\s\d{2}:\d{2}:\d{2}\.\d{3}$/, - `Expected trained model row created at time to have same format as 'Dec 5, 2019 @ 12:28:34.594' (got '${modelRow.createdAt}')` - ); } public async assertTableIsPopulated() { @@ -584,9 +572,6 @@ export function TrainedModelsTableProvider( await mlCommonUI.waitForRefreshButtonEnabled(); - await mlCommonUI.assertLastToastHeader( - `Deployment for "${modelId}" has been started successfully.` - ); await this.waitForModelsToLoad(); await retry.tryForTime( @@ -612,10 +597,6 @@ export function TrainedModelsTableProvider( public async stopDeployment(modelId: string) { await this.clickStopDeploymentAction(modelId); await mlCommonUI.waitForRefreshButtonEnabled(); - await mlCommonUI.assertLastToastHeader( - `Deployment for "${modelId}" has been stopped successfully.` - ); - await mlCommonUI.waitForRefreshButtonEnabled(); } public async openStartDeploymentModal(modelId: string) { diff --git a/x-pack/test/functional_execution_context/tests/browser.ts b/x-pack/test/functional_execution_context/tests/browser.ts index e1d7ba6a3b965..c7228528ee756 100644 --- a/x-pack/test/functional_execution_context/tests/browser.ts +++ b/x-pack/test/functional_execution_context/tests/browser.ts @@ -85,14 +85,9 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { type: 'application', name: 'discover', url: '/app/discover', - child: { - name: 'discover', - url: '/app/discover', - type: 'application', - page: 'app', - id: 'new', - description: 'fetch documents', - }, + page: 'app', + id: 'new', + description: 'fetch documents', }), }); }); @@ -105,20 +100,15 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { type: 'application', name: 'discover', url: '/app/discover', + page: 'app', + id: 'new', + description: 'fetch chart data and total hits', child: { - name: 'discover', - url: '/app/discover', - type: 'application', - page: 'app', - id: 'new', - description: 'fetch chart data and total hits', - child: { - type: 'lens', - name: 'lnsXY', - id: 'unifiedHistogramLensComponent', - description: 'Edit visualization', - url: '/app/lens#/edit_by_value', - }, + type: 'lens', + name: 'lnsXY', + id: 'unifiedHistogramLensComponent', + description: 'Edit visualization', + url: '/app/lens#/edit_by_value', }, }), }); @@ -185,9 +175,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { it('propagates to Elasticsearch via "x-opaque-id" header', async () => { await logContains({ description: 'execution context propagates to Elasticsearch via "x-opaque-id" header', - predicate: checkHttpRequestId( - 'dashboard:dashboards:7adfa750-4c81-11e8-b3d7-01146121b73d;lens:lnsXY:086ac2e9-dd16-4b45-92b8-1e43ff7e3f65' - ), + predicate: checkHttpRequestId('lens:lnsXY:086ac2e9-dd16-4b45-92b8-1e43ff7e3f65'), }); }); @@ -195,23 +183,18 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { await logContains({ description: 'execution context propagates to Kibana logs', predicate: checkExecutionContextEntry({ - type: 'application', + type: 'dashboard', name: 'dashboards', url: '/app/dashboards', + page: 'app', + id: '7adfa750-4c81-11e8-b3d7-01146121b73d', + description: '[Flights] Global Flight Dashboard', child: { - name: 'dashboards', - url: '/app/dashboards', - type: 'dashboard', - page: 'app', - id: '7adfa750-4c81-11e8-b3d7-01146121b73d', - description: '[Flights] Global Flight Dashboard', - child: { - type: 'lens', - name: 'lnsXY', - id: '086ac2e9-dd16-4b45-92b8-1e43ff7e3f65', - description: '[Flights] Flight count', - url: '/app/lens#/edit_by_value', - }, + type: 'lens', + name: 'lnsXY', + id: '086ac2e9-dd16-4b45-92b8-1e43ff7e3f65', + description: '[Flights] Flight count', + url: '/app/lens#/edit_by_value', }, }), }); @@ -222,9 +205,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { it('propagates to Elasticsearch via "x-opaque-id" header', async () => { await logContains({ description: 'execution context propagates to Elasticsearch via "x-opaque-id" header', - predicate: checkHttpRequestId( - 'dashboard:dashboards:7adfa750-4c81-11e8-b3d7-01146121b73d;lens:lnsMetric:b766e3b8-4544-46ed-99e6-9ecc4847e2a2' - ), + predicate: checkHttpRequestId('lens:lnsMetric:b766e3b8-4544-46ed-99e6-9ecc4847e2a2'), }); }); @@ -232,23 +213,18 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { await logContains({ description: 'execution context propagates to Kibana logs', predicate: checkExecutionContextEntry({ + type: 'dashboard', name: 'dashboards', url: '/app/dashboards', - type: 'application', + page: 'app', + id: '7adfa750-4c81-11e8-b3d7-01146121b73d', + description: '[Flights] Global Flight Dashboard', child: { - name: 'dashboards', - url: '/app/dashboards', - type: 'dashboard', - page: 'app', - id: '7adfa750-4c81-11e8-b3d7-01146121b73d', - description: '[Flights] Global Flight Dashboard', - child: { - type: 'lens', - name: 'lnsMetric', - id: 'b766e3b8-4544-46ed-99e6-9ecc4847e2a2', - description: '', - url: '/app/lens#/edit_by_value', - }, + type: 'lens', + name: 'lnsMetric', + id: 'b766e3b8-4544-46ed-99e6-9ecc4847e2a2', + description: '', + url: '/app/lens#/edit_by_value', }, }), }); @@ -260,7 +236,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { await logContains({ description: 'execution context propagates to Elasticsearch via "x-opaque-id" header', predicate: checkHttpRequestId( - 'dashboard:dashboards:7adfa750-4c81-11e8-b3d7-01146121b73d;lens:lnsDatatable:fb86b32f-fb7a-45cf-9511-f366fef51bbd' + 'lens:lnsDatatable:fb86b32f-fb7a-45cf-9511-f366fef51bbd' ), }); }); @@ -269,23 +245,18 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { await logContains({ description: 'execution context propagates to Kibana logs', predicate: checkExecutionContextEntry({ + type: 'dashboard', name: 'dashboards', url: '/app/dashboards', - type: 'application', + page: 'app', + id: '7adfa750-4c81-11e8-b3d7-01146121b73d', + description: '[Flights] Global Flight Dashboard', child: { - name: 'dashboards', - url: '/app/dashboards', - type: 'dashboard', - page: 'app', - id: '7adfa750-4c81-11e8-b3d7-01146121b73d', - description: '[Flights] Global Flight Dashboard', - child: { - type: 'lens', - name: 'lnsDatatable', - id: 'fb86b32f-fb7a-45cf-9511-f366fef51bbd', - description: 'Cities by delay, cancellation', - url: '/app/lens#/edit_by_value', - }, + type: 'lens', + name: 'lnsDatatable', + id: 'fb86b32f-fb7a-45cf-9511-f366fef51bbd', + description: 'Cities by delay, cancellation', + url: '/app/lens#/edit_by_value', }, }), }); @@ -296,9 +267,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { it('propagates to Elasticsearch via "x-opaque-id" header', async () => { await logContains({ description: 'execution context propagates to Elasticsearch via "x-opaque-id" header', - predicate: checkHttpRequestId( - 'dashboard:dashboards:7adfa750-4c81-11e8-b3d7-01146121b73d;lens:lnsPie:5d53db36-2d5a-4adc-af7b-cec4c1a294e0' - ), + predicate: checkHttpRequestId('lens:lnsPie:5d53db36-2d5a-4adc-af7b-cec4c1a294e0'), }); }); @@ -306,23 +275,18 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { await logContains({ description: 'execution context propagates to Kibana logs', predicate: checkExecutionContextEntry({ + type: 'dashboard', name: 'dashboards', url: '/app/dashboards', - type: 'application', + page: 'app', + id: '7adfa750-4c81-11e8-b3d7-01146121b73d', + description: '[Flights] Global Flight Dashboard', child: { - name: 'dashboards', - url: '/app/dashboards', - type: 'dashboard', - page: 'app', - id: '7adfa750-4c81-11e8-b3d7-01146121b73d', - description: '[Flights] Global Flight Dashboard', - child: { - type: 'lens', - name: 'lnsPie', - id: '5d53db36-2d5a-4adc-af7b-cec4c1a294e0', - description: '[Flights] Delay Type', - url: '/app/lens#/edit_by_value', - }, + type: 'lens', + name: 'lnsPie', + id: '5d53db36-2d5a-4adc-af7b-cec4c1a294e0', + description: '[Flights] Delay Type', + url: '/app/lens#/edit_by_value', }, }), }); @@ -334,9 +298,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { it('propagates to Elasticsearch via "x-opaque-id" header', async () => { await logContains({ description: 'execution context propagates to Elasticsearch via "x-opaque-id" header', - predicate: checkHttpRequestId( - 'dashboard:dashboards:7adfa750-4c81-11e8-b3d7-01146121b73d;search:discover:571aaf70-4c88-11e8-b3d7-01146121b73d' - ), + predicate: checkHttpRequestId('search:discover:571aaf70-4c88-11e8-b3d7-01146121b73d'), }); }); @@ -344,23 +306,18 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { await logContains({ description: 'execution context propagates to Kibana logs', predicate: checkExecutionContextEntry({ - type: 'application', + type: 'dashboard', name: 'dashboards', url: '/app/dashboards', + page: 'app', + id: '7adfa750-4c81-11e8-b3d7-01146121b73d', + description: '[Flights] Global Flight Dashboard', child: { - type: 'dashboard', - name: 'dashboards', - url: '/app/dashboards', - page: 'app', - id: '7adfa750-4c81-11e8-b3d7-01146121b73d', - description: '[Flights] Global Flight Dashboard', - child: { - type: 'search', - name: 'discover', - id: '571aaf70-4c88-11e8-b3d7-01146121b73d', - description: '[Flights] Flight Log', - url: '/app/discover#/view/571aaf70-4c88-11e8-b3d7-01146121b73d', - }, + type: 'search', + name: 'discover', + id: '571aaf70-4c88-11e8-b3d7-01146121b73d', + description: '[Flights] Flight Log', + url: '/app/discover#/view/571aaf70-4c88-11e8-b3d7-01146121b73d', }, }), }); diff --git a/x-pack/test/functional_search/config.ts b/x-pack/test/functional_search/config.ts new file mode 100644 index 0000000000000..f997aaea7c5e2 --- /dev/null +++ b/x-pack/test/functional_search/config.ts @@ -0,0 +1,31 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { FtrConfigProviderContext } from '@kbn/test'; + +/** + * NOTE: The solution view is currently only available in the cloud environment. + * This test suite fakes a cloud environement by setting the cloud.id and cloud.base_url + */ + +export default async function ({ readConfigFile }: FtrConfigProviderContext) { + const functionalConfig = await readConfigFile(require.resolve('../functional/config.base.js')); + + return { + ...functionalConfig.getAll(), + testFiles: [require.resolve('.')], + kbnTestServer: { + ...functionalConfig.get('kbnTestServer'), + serverArgs: [ + ...functionalConfig.get('kbnTestServer.serverArgs'), + // Note: the base64 string in the cloud.id config contains the ES endpoint required in the functional tests + '--xpack.cloud.id=ftr_fake_cloud_id:aGVsbG8uY29tOjQ0MyRFUzEyM2FiYyRrYm4xMjNhYmM=', + '--xpack.cloud.base_url=https://cloud.elastic.co', + ], + }, + }; +} diff --git a/x-pack/test/functional_search/ftr_provider_context.ts b/x-pack/test/functional_search/ftr_provider_context.ts new file mode 100644 index 0000000000000..d6c0afa5ceffd --- /dev/null +++ b/x-pack/test/functional_search/ftr_provider_context.ts @@ -0,0 +1,13 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { GenericFtrProviderContext } from '@kbn/test'; +import { pageObjects } from '../functional/page_objects'; +import { services } from './services'; + +export type FtrProviderContext = GenericFtrProviderContext; +export { pageObjects }; diff --git a/x-pack/test/functional_search/index.ts b/x-pack/test/functional_search/index.ts new file mode 100644 index 0000000000000..149b3dbcf7eca --- /dev/null +++ b/x-pack/test/functional_search/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. + */ +/* eslint-disable import/no-default-export */ + +import { FtrProviderContext } from './ftr_provider_context'; + +export default ({ loadTestFile }: FtrProviderContext): void => { + describe('Search solution tests', function () { + loadTestFile(require.resolve('./tests/solution_navigation')); + }); +}; diff --git a/x-pack/test/functional_search/services.ts b/x-pack/test/functional_search/services.ts new file mode 100644 index 0000000000000..9508ce5eba16d --- /dev/null +++ b/x-pack/test/functional_search/services.ts @@ -0,0 +1,10 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { services as functionalServices } from '../functional/services'; + +export const services = functionalServices; diff --git a/x-pack/test/functional_search/tests/solution_navigation.ts b/x-pack/test/functional_search/tests/solution_navigation.ts new file mode 100644 index 0000000000000..8a06ad1193372 --- /dev/null +++ b/x-pack/test/functional_search/tests/solution_navigation.ts @@ -0,0 +1,314 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { FtrProviderContext } from '../ftr_provider_context'; + +export default function searchSolutionNavigation({ + getPageObjects, + getService, +}: FtrProviderContext) { + const { common, solutionNavigation } = getPageObjects(['common', 'solutionNavigation']); + const spaces = getService('spaces'); + const browser = getService('browser'); + + describe('Search Solution Navigation', () => { + let cleanUp: () => Promise; + let spaceCreated: { id: string } = { id: '' }; + + before(async () => { + // Navigate to the spaces management page which will log us in Kibana + await common.navigateToUrl('management', 'kibana/spaces', { + shouldUseHashForSubUrl: false, + }); + + // Create a space with the search solution and navigate to its home page + ({ cleanUp, space: spaceCreated } = await spaces.create({ solution: 'es' })); + await browser.navigateTo(spaces.getRootUrl(spaceCreated.id)); + }); + + after(async () => { + // Clean up space created + await cleanUp(); + }); + + it('renders expected side nav items', async () => { + // Verify all expected top-level links exist + await solutionNavigation.sidenav.expectLinkExists({ text: 'Overview' }); + await solutionNavigation.sidenav.expectLinkExists({ text: 'Dev Tools' }); + await solutionNavigation.sidenav.expectLinkExists({ text: 'Discover' }); + await solutionNavigation.sidenav.expectLinkExists({ text: 'Dashboards' }); + await solutionNavigation.sidenav.expectLinkExists({ text: 'Indices' }); + await solutionNavigation.sidenav.expectLinkExists({ text: 'Connectors' }); + await solutionNavigation.sidenav.expectLinkExists({ text: 'Web crawlers' }); + await solutionNavigation.sidenav.expectLinkExists({ text: 'Playground' }); + await solutionNavigation.sidenav.expectLinkExists({ text: 'Search applications' }); + await solutionNavigation.sidenav.expectLinkExists({ text: 'Behavioral Analytics' }); + await solutionNavigation.sidenav.expectLinkExists({ text: 'App Search' }); + await solutionNavigation.sidenav.expectLinkExists({ text: 'Workplace Search' }); + await solutionNavigation.sidenav.expectLinkExists({ text: 'Other tools' }); + }); + + it('has expected navigation', async () => { + const expectNoPageReload = await solutionNavigation.createNoPageReloadCheck(); + + // check side nav links + await solutionNavigation.sidenav.expectSectionExists('search_project_nav'); + await solutionNavigation.sidenav.expectLinkActive({ + deepLinkId: 'enterpriseSearch', + }); + await solutionNavigation.breadcrumbs.expectBreadcrumbExists({ + deepLinkId: 'enterpriseSearch', + }); + + // check Dev tools + await solutionNavigation.sidenav.clickLink({ + deepLinkId: 'dev_tools', + }); + await solutionNavigation.sidenav.expectLinkActive({ + deepLinkId: 'dev_tools', + }); + await solutionNavigation.breadcrumbs.expectBreadcrumbExists({ text: 'Dev Tools' }); + await solutionNavigation.breadcrumbs.expectBreadcrumbExists({ + deepLinkId: 'dev_tools', + }); + + // check Kibana + // > Discover + await solutionNavigation.sidenav.clickLink({ + deepLinkId: 'discover', + }); + await solutionNavigation.sidenav.expectLinkActive({ + deepLinkId: 'discover', + }); + await solutionNavigation.breadcrumbs.expectBreadcrumbExists({ text: 'Kibana' }); + await solutionNavigation.breadcrumbs.expectBreadcrumbExists({ text: 'Discover' }); + await solutionNavigation.breadcrumbs.expectBreadcrumbExists({ + deepLinkId: 'discover', + }); + // > Dashboards + await solutionNavigation.sidenav.clickLink({ + deepLinkId: 'dashboards', + }); + await solutionNavigation.sidenav.expectLinkActive({ + deepLinkId: 'dashboards', + }); + await solutionNavigation.breadcrumbs.expectBreadcrumbExists({ text: 'Kibana' }); + await solutionNavigation.breadcrumbs.expectBreadcrumbExists({ text: 'Dashboards' }); + await solutionNavigation.breadcrumbs.expectBreadcrumbExists({ + deepLinkId: 'dashboards', + }); + + // check the Content + // > Indices section + await solutionNavigation.sidenav.clickLink({ + deepLinkId: 'enterpriseSearchContent:searchIndices', + }); + await solutionNavigation.sidenav.expectLinkActive({ + deepLinkId: 'enterpriseSearchContent:searchIndices', + }); + await solutionNavigation.breadcrumbs.expectBreadcrumbExists({ text: 'Content' }); + await solutionNavigation.breadcrumbs.expectBreadcrumbExists({ text: 'Indices' }); + await solutionNavigation.breadcrumbs.expectBreadcrumbExists({ + deepLinkId: 'enterpriseSearchContent:searchIndices', + }); + // > Connectors + await solutionNavigation.sidenav.clickLink({ + deepLinkId: 'enterpriseSearchContent:connectors', + }); + await solutionNavigation.sidenav.expectLinkActive({ + deepLinkId: 'enterpriseSearchContent:connectors', + }); + await solutionNavigation.breadcrumbs.expectBreadcrumbExists({ text: 'Content' }); + await solutionNavigation.breadcrumbs.expectBreadcrumbExists({ text: 'Connectors' }); + await solutionNavigation.breadcrumbs.expectBreadcrumbExists({ + deepLinkId: 'enterpriseSearchContent:connectors', + }); + // > Web Crawlers + await solutionNavigation.sidenav.clickLink({ + deepLinkId: 'enterpriseSearchContent:webCrawlers', + }); + await solutionNavigation.sidenav.expectLinkActive({ + deepLinkId: 'enterpriseSearchContent:webCrawlers', + }); + await solutionNavigation.breadcrumbs.expectBreadcrumbExists({ text: 'Content' }); + await solutionNavigation.breadcrumbs.expectBreadcrumbExists({ text: 'Web crawlers' }); + await solutionNavigation.breadcrumbs.expectBreadcrumbExists({ + deepLinkId: 'enterpriseSearchContent:webCrawlers', + }); + + // check Build + // > Playground + await solutionNavigation.sidenav.clickLink({ + deepLinkId: 'enterpriseSearchApplications:playground', + }); + await solutionNavigation.sidenav.expectLinkActive({ + deepLinkId: 'enterpriseSearchApplications:playground', + }); + await solutionNavigation.breadcrumbs.expectBreadcrumbExists({ text: 'Build' }); + await solutionNavigation.breadcrumbs.expectBreadcrumbExists({ text: 'Playground' }); + await solutionNavigation.breadcrumbs.expectBreadcrumbExists({ + deepLinkId: 'enterpriseSearchApplications:playground', + }); + // > Search applications + await solutionNavigation.sidenav.clickLink({ + deepLinkId: 'enterpriseSearchApplications:searchApplications', + }); + await solutionNavigation.sidenav.expectLinkActive({ + deepLinkId: 'enterpriseSearchApplications:searchApplications', + }); + await solutionNavigation.breadcrumbs.expectBreadcrumbExists({ text: 'Build' }); + await solutionNavigation.breadcrumbs.expectBreadcrumbExists({ + text: 'Search applications', + }); + await solutionNavigation.breadcrumbs.expectBreadcrumbExists({ + deepLinkId: 'enterpriseSearchApplications:searchApplications', + }); + // > Behavioral Analytics + await solutionNavigation.sidenav.clickLink({ + deepLinkId: 'enterpriseSearchAnalytics', + }); + await solutionNavigation.sidenav.expectLinkActive({ + deepLinkId: 'enterpriseSearchAnalytics', + }); + await solutionNavigation.breadcrumbs.expectBreadcrumbExists({ text: 'Build' }); + await solutionNavigation.breadcrumbs.expectBreadcrumbExists({ + text: 'Behavioral Analytics', + }); + await solutionNavigation.breadcrumbs.expectBreadcrumbExists({ + deepLinkId: 'enterpriseSearchAnalytics', + }); + + // check Relevance + // > Inference Endpoints + // TODO: FTRs don't have enterprise license, so inference endpoints not shown + // await solutionNavigation.sidenav.clickLink({ + // deepLinkId: 'enterpriseSearchRelevance:inferenceEndpoints', + // }); + // await solutionNavigation.sidenav.expectLinkActive({ + // deepLinkId: 'enterpriseSearchRelevance:inferenceEndpoints', + // }); + // await solutionNavigation.breadcrumbs.expectBreadcrumbExists({ text: 'Relevance' }); + // await solutionNavigation.breadcrumbs.expectBreadcrumbExists({ + // text: 'Inference Endpoints', + // }); + // await solutionNavigation.breadcrumbs.expectBreadcrumbExists({ + // deepLinkId: 'enterpriseSearchRelevance:inferenceEndpoints', + // }); + + // check Enterprise Search + // > App Search + await solutionNavigation.sidenav.clickLink({ + deepLinkId: 'appSearch:engines', + }); + await solutionNavigation.sidenav.expectLinkActive({ + deepLinkId: 'appSearch:engines', + }); + // ent-search node not running for FTRs, so we see setup guide without breadcrumbs + // await solutionNavigation.breadcrumbs.expectBreadcrumbExists({ + // text: 'App Search', + // }); + await solutionNavigation.breadcrumbs.expectBreadcrumbExists({ + deepLinkId: 'appSearch:engines', + }); + // > Workplace Search + await solutionNavigation.sidenav.clickLink({ + deepLinkId: 'workplaceSearch', + }); + await solutionNavigation.sidenav.expectLinkActive({ + deepLinkId: 'workplaceSearch', + }); + // ent-search node not running for FTRs, so we see setup guide without breadcrumbs + // await solutionNavigation.breadcrumbs.expectBreadcrumbExists({ + // text: 'Workplace Search', + // }); + await solutionNavigation.breadcrumbs.expectBreadcrumbExists({ + deepLinkId: 'workplaceSearch', + }); + + // Other tools + await solutionNavigation.sidenav.openSection('search_project_nav.otherTools'); + // > Maps + await solutionNavigation.sidenav.clickLink({ + deepLinkId: 'maps', + }); + await solutionNavigation.sidenav.expectLinkActive({ + deepLinkId: 'maps', + }); + await solutionNavigation.breadcrumbs.expectBreadcrumbExists({ text: 'Other tools' }); + await solutionNavigation.breadcrumbs.expectBreadcrumbExists({ + text: 'Maps', + }); + await solutionNavigation.breadcrumbs.expectBreadcrumbExists({ + deepLinkId: 'maps', + }); + // > Canvas + await solutionNavigation.sidenav.clickLink({ + deepLinkId: 'canvas', + }); + await solutionNavigation.sidenav.expectLinkActive({ + deepLinkId: 'canvas', + }); + await solutionNavigation.breadcrumbs.expectBreadcrumbExists({ text: 'Other tools' }); + await solutionNavigation.breadcrumbs.expectBreadcrumbExists({ + text: 'Canvas', + }); + await solutionNavigation.breadcrumbs.expectBreadcrumbExists({ + deepLinkId: 'canvas', + }); + // > Graph + await solutionNavigation.sidenav.clickLink({ + deepLinkId: 'graph', + }); + await solutionNavigation.sidenav.expectLinkActive({ + deepLinkId: 'graph', + }); + await solutionNavigation.breadcrumbs.expectBreadcrumbExists({ text: 'Other tools' }); + await solutionNavigation.breadcrumbs.expectBreadcrumbExists({ + text: 'Graph', + }); + await solutionNavigation.breadcrumbs.expectBreadcrumbExists({ + deepLinkId: 'graph', + }); + await solutionNavigation.sidenav.closeSection('search_project_nav.otherTools'); + + await expectNoPageReload(); + }); + + it('renders only expected items', async () => { + await solutionNavigation.sidenav.openSection('search_project_nav.otherTools'); + await solutionNavigation.sidenav.openSection('project_settings_project_nav'); + await solutionNavigation.sidenav.expectOnlyDefinedLinks([ + 'search_project_nav', + 'enterpriseSearch', + 'dev_tools', + 'kibana', + 'discover', + 'dashboards', + 'content', + 'enterpriseSearchContent:searchIndices', + 'enterpriseSearchContent:connectors', + 'enterpriseSearchContent:webCrawlers', + 'build', + 'enterpriseSearchApplications:playground', + 'enterpriseSearchApplications:searchApplications', + 'enterpriseSearchAnalytics', + // 'relevance', + // 'enterpriseSearchRelevance:inferenceEndpoints', + 'entsearch', + 'appSearch:engines', + 'workplaceSearch', + 'otherTools', + 'maps', + 'canvas', + 'graph', + 'project_settings_project_nav', + 'ml:modelManagement', + 'stack_management', + ]); + }); + }); +} diff --git a/x-pack/test/reporting_functional/reporting_and_security/management.ts b/x-pack/test/reporting_functional/reporting_and_security/management.ts index 570c1bbdda4c7..b1a6c107b9bb7 100644 --- a/x-pack/test/reporting_functional/reporting_and_security/management.ts +++ b/x-pack/test/reporting_functional/reporting_and_security/management.ts @@ -57,6 +57,7 @@ export default ({ getService, getPageObjects }: FtrProviderContext) => { }); // FLAKY: https://github.com/elastic/kibana/issues/195144 + // FLAKY: https://github.com/elastic/kibana/issues/194731 describe.skip('Download report', () => { // use archived reports to allow reporting_user to view report jobs they've created before('log in as reporting user', async () => { diff --git a/x-pack/test/security_api_integration/anonymous.config.ts b/x-pack/test/security_api_integration/anonymous.config.ts index a53584b547efd..396514f540d6d 100644 --- a/x-pack/test/security_api_integration/anonymous.config.ts +++ b/x-pack/test/security_api_integration/anonymous.config.ts @@ -5,9 +5,10 @@ * 2.0. */ -import { FtrConfigProviderContext } from '@kbn/test'; import { resolve } from 'path'; +import type { FtrConfigProviderContext } from '@kbn/test'; + export default async function ({ readConfigFile }: FtrConfigProviderContext) { const kibanaAPITestsConfig = await readConfigFile( require.resolve('@kbn/test-suites-src/api_integration/config') diff --git a/x-pack/test/security_api_integration/anonymous_es_anonymous.config.ts b/x-pack/test/security_api_integration/anonymous_es_anonymous.config.ts index 60691769729fa..78d49634bc472 100644 --- a/x-pack/test/security_api_integration/anonymous_es_anonymous.config.ts +++ b/x-pack/test/security_api_integration/anonymous_es_anonymous.config.ts @@ -5,7 +5,7 @@ * 2.0. */ -import { FtrConfigProviderContext } from '@kbn/test'; +import type { FtrConfigProviderContext } from '@kbn/test'; export default async function ({ readConfigFile }: FtrConfigProviderContext) { const anonymousAPITestsConfig = await readConfigFile(require.resolve('./anonymous.config.ts')); diff --git a/x-pack/test/security_api_integration/api_keys.config.ts b/x-pack/test/security_api_integration/api_keys.config.ts index a1ec0e428845a..23295e91a8741 100644 --- a/x-pack/test/security_api_integration/api_keys.config.ts +++ b/x-pack/test/security_api_integration/api_keys.config.ts @@ -5,7 +5,8 @@ * 2.0. */ -import { FtrConfigProviderContext } from '@kbn/test'; +import type { FtrConfigProviderContext } from '@kbn/test'; + import { services } from './services'; export default async function ({ readConfigFile }: FtrConfigProviderContext) { diff --git a/x-pack/test/security_api_integration/audit.config.ts b/x-pack/test/security_api_integration/audit.config.ts index 052f241470bee..858aeedbebcdb 100644 --- a/x-pack/test/security_api_integration/audit.config.ts +++ b/x-pack/test/security_api_integration/audit.config.ts @@ -6,7 +6,8 @@ */ import { resolve } from 'path'; -import { FtrConfigProviderContext } from '@kbn/test'; + +import type { FtrConfigProviderContext } from '@kbn/test'; export default async function ({ readConfigFile }: FtrConfigProviderContext) { const xPackAPITestsConfig = await readConfigFile(require.resolve('../api_integration/config.ts')); diff --git a/x-pack/test/security_api_integration/chips.config.ts b/x-pack/test/security_api_integration/chips.config.ts index b91a4ebfcc3e9..2aedcc767b615 100644 --- a/x-pack/test/security_api_integration/chips.config.ts +++ b/x-pack/test/security_api_integration/chips.config.ts @@ -5,9 +5,10 @@ * 2.0. */ -import { FtrConfigProviderContext } from '@kbn/test'; import { resolve } from 'path'; +import type { FtrConfigProviderContext } from '@kbn/test'; + export default async function ({ readConfigFile }: FtrConfigProviderContext) { const kibanaAPITestsConfig = await readConfigFile( require.resolve('@kbn/test-suites-src/api_integration/config') diff --git a/x-pack/test/security_api_integration/ftr_provider_context.d.ts b/x-pack/test/security_api_integration/ftr_provider_context.d.ts index 647664d640466..b05bbc8f6318d 100644 --- a/x-pack/test/security_api_integration/ftr_provider_context.d.ts +++ b/x-pack/test/security_api_integration/ftr_provider_context.d.ts @@ -5,7 +5,8 @@ * 2.0. */ -import { GenericFtrProviderContext } from '@kbn/test'; -import { services } from './services'; +import type { GenericFtrProviderContext } from '@kbn/test'; + +import type { services } from './services'; export type FtrProviderContext = GenericFtrProviderContext; diff --git a/x-pack/test/security_api_integration/http_bearer.config.ts b/x-pack/test/security_api_integration/http_bearer.config.ts index 87f0b39cbb648..6a01822811d20 100644 --- a/x-pack/test/security_api_integration/http_bearer.config.ts +++ b/x-pack/test/security_api_integration/http_bearer.config.ts @@ -5,8 +5,10 @@ * 2.0. */ -import { FtrConfigProviderContext } from '@kbn/test'; import { resolve } from 'path'; + +import type { FtrConfigProviderContext } from '@kbn/test'; + import { services } from './services'; export default async function ({ readConfigFile }: FtrConfigProviderContext) { diff --git a/x-pack/test/security_api_integration/http_no_auth_providers.config.ts b/x-pack/test/security_api_integration/http_no_auth_providers.config.ts index b46bbe3134dd4..5df4eefe1212a 100644 --- a/x-pack/test/security_api_integration/http_no_auth_providers.config.ts +++ b/x-pack/test/security_api_integration/http_no_auth_providers.config.ts @@ -5,7 +5,8 @@ * 2.0. */ -import { FtrConfigProviderContext } from '@kbn/test'; +import type { FtrConfigProviderContext } from '@kbn/test'; + import { services } from './services'; export default async function ({ readConfigFile }: FtrConfigProviderContext) { diff --git a/x-pack/test/security_api_integration/kerberos.config.ts b/x-pack/test/security_api_integration/kerberos.config.ts index ba948efde4cd9..28bed3bfe25ee 100644 --- a/x-pack/test/security_api_integration/kerberos.config.ts +++ b/x-pack/test/security_api_integration/kerberos.config.ts @@ -6,7 +6,9 @@ */ import { resolve } from 'path'; -import { FtrConfigProviderContext } from '@kbn/test'; + +import type { FtrConfigProviderContext } from '@kbn/test'; + import { services } from './services'; export default async function ({ readConfigFile }: FtrConfigProviderContext) { diff --git a/x-pack/test/security_api_integration/kerberos_anonymous_access.config.ts b/x-pack/test/security_api_integration/kerberos_anonymous_access.config.ts index 355f0e90bcd91..d315ad0cff42e 100644 --- a/x-pack/test/security_api_integration/kerberos_anonymous_access.config.ts +++ b/x-pack/test/security_api_integration/kerberos_anonymous_access.config.ts @@ -5,7 +5,7 @@ * 2.0. */ -import { FtrConfigProviderContext } from '@kbn/test'; +import type { FtrConfigProviderContext } from '@kbn/test'; export default async function ({ readConfigFile }: FtrConfigProviderContext) { const kerberosAPITestsConfig = await readConfigFile(require.resolve('./kerberos.config.ts')); diff --git a/x-pack/test/security_api_integration/login_selector.config.ts b/x-pack/test/security_api_integration/login_selector.config.ts index 47371973028f0..9517cb2a45e8e 100644 --- a/x-pack/test/security_api_integration/login_selector.config.ts +++ b/x-pack/test/security_api_integration/login_selector.config.ts @@ -7,8 +7,9 @@ import { readFileSync } from 'fs'; import { resolve } from 'path'; + import { CA_CERT_PATH, KBN_CERT_PATH, KBN_KEY_PATH } from '@kbn/dev-utils'; -import { FtrConfigProviderContext } from '@kbn/test'; +import type { FtrConfigProviderContext } from '@kbn/test'; export default async function ({ readConfigFile }: FtrConfigProviderContext) { const kibanaAPITestsConfig = await readConfigFile( diff --git a/x-pack/test/security_api_integration/oidc.config.ts b/x-pack/test/security_api_integration/oidc.config.ts index 43cc5292ea5c2..28baa97f1dcc4 100644 --- a/x-pack/test/security_api_integration/oidc.config.ts +++ b/x-pack/test/security_api_integration/oidc.config.ts @@ -6,7 +6,9 @@ */ import { resolve } from 'path'; -import { FtrConfigProviderContext } from '@kbn/test'; + +import type { FtrConfigProviderContext } from '@kbn/test'; + import { services } from './services'; export default async function ({ readConfigFile }: FtrConfigProviderContext) { diff --git a/x-pack/test/security_api_integration/oidc.http2.config.ts b/x-pack/test/security_api_integration/oidc.http2.config.ts index 957813ceb046e..034e008132890 100644 --- a/x-pack/test/security_api_integration/oidc.http2.config.ts +++ b/x-pack/test/security_api_integration/oidc.http2.config.ts @@ -5,8 +5,8 @@ * 2.0. */ -import { FtrConfigProviderContext } from '@kbn/test'; import { CA_CERT_PATH } from '@kbn/dev-utils'; +import type { FtrConfigProviderContext } from '@kbn/test'; import { configureHTTP2 } from '@kbn/test-suites-src/common/configure_http2'; export default async function ({ readConfigFile }: FtrConfigProviderContext) { diff --git a/x-pack/test/security_api_integration/oidc_implicit_flow.config.ts b/x-pack/test/security_api_integration/oidc_implicit_flow.config.ts index 3b9edcbec6826..94841661f6ed1 100644 --- a/x-pack/test/security_api_integration/oidc_implicit_flow.config.ts +++ b/x-pack/test/security_api_integration/oidc_implicit_flow.config.ts @@ -5,7 +5,7 @@ * 2.0. */ -import { FtrConfigProviderContext } from '@kbn/test'; +import type { FtrConfigProviderContext } from '@kbn/test'; export default async function ({ readConfigFile }: FtrConfigProviderContext) { const oidcAPITestsConfig = await readConfigFile(require.resolve('./oidc.config.ts')); diff --git a/x-pack/test/security_api_integration/packages/helpers/saml/saml_tools.ts b/x-pack/test/security_api_integration/packages/helpers/saml/saml_tools.ts index 255625082407b..b1dd8f851caf0 100644 --- a/x-pack/test/security_api_integration/packages/helpers/saml/saml_tools.ts +++ b/x-pack/test/security_api_integration/packages/helpers/saml/saml_tools.ts @@ -9,10 +9,11 @@ import crypto from 'crypto'; import fs from 'fs'; import { stringify } from 'query-string'; import url from 'url'; -import zlib from 'zlib'; import { promisify } from 'util'; -import { parseString } from 'xml2js'; import { SignedXml } from 'xml-crypto'; +import { parseString } from 'xml2js'; +import zlib from 'zlib'; + import { KBN_KEY_PATH } from '@kbn/dev-utils'; /** diff --git a/x-pack/test/security_api_integration/pki.config.ts b/x-pack/test/security_api_integration/pki.config.ts index 49c23c8d80b79..e10be72339984 100644 --- a/x-pack/test/security_api_integration/pki.config.ts +++ b/x-pack/test/security_api_integration/pki.config.ts @@ -6,8 +6,10 @@ */ import { resolve } from 'path'; -import { FtrConfigProviderContext } from '@kbn/test'; + import { CA_CERT_PATH, KBN_CERT_PATH, KBN_KEY_PATH } from '@kbn/dev-utils'; +import type { FtrConfigProviderContext } from '@kbn/test'; + import { services } from './services'; export default async function ({ readConfigFile }: FtrConfigProviderContext) { diff --git a/x-pack/test/security_api_integration/plugins/audit_log/server/plugin.ts b/x-pack/test/security_api_integration/plugins/audit_log/server/plugin.ts index d7495dfd80f8d..647faccd0c0d0 100644 --- a/x-pack/test/security_api_integration/plugins/audit_log/server/plugin.ts +++ b/x-pack/test/security_api_integration/plugins/audit_log/server/plugin.ts @@ -5,7 +5,7 @@ * 2.0. */ -import { Plugin, CoreSetup } from '@kbn/core/server'; +import type { CoreSetup, Plugin } from '@kbn/core/server'; export class AuditTrailTestPlugin implements Plugin { public setup(core: CoreSetup) { diff --git a/x-pack/test/security_api_integration/plugins/oidc_provider/server/index.ts b/x-pack/test/security_api_integration/plugins/oidc_provider/server/index.ts index 82e460a6a4c27..b049f74d0a69f 100644 --- a/x-pack/test/security_api_integration/plugins/oidc_provider/server/index.ts +++ b/x-pack/test/security_api_integration/plugins/oidc_provider/server/index.ts @@ -5,7 +5,8 @@ * 2.0. */ -import type { PluginInitializer, Plugin } from '@kbn/core/server'; +import type { Plugin, PluginInitializer } from '@kbn/core/server'; + import { initRoutes } from './init_routes'; export const plugin: PluginInitializer = async (): Promise => ({ diff --git a/x-pack/test/security_api_integration/plugins/saml_provider/server/index.ts b/x-pack/test/security_api_integration/plugins/saml_provider/server/index.ts index ad297baf7246f..865240e32e9f0 100644 --- a/x-pack/test/security_api_integration/plugins/saml_provider/server/index.ts +++ b/x-pack/test/security_api_integration/plugins/saml_provider/server/index.ts @@ -5,8 +5,9 @@ * 2.0. */ -import type { PluginInitializer, Plugin } from '@kbn/core/server'; -import { CloudSetup } from '@kbn/cloud-plugin/server'; +import type { CloudSetup } from '@kbn/cloud-plugin/server'; +import type { Plugin, PluginInitializer } from '@kbn/core/server'; + import { initRoutes } from './init_routes'; export interface PluginSetupDependencies { diff --git a/x-pack/test/security_api_integration/plugins/saml_provider/server/init_routes.ts b/x-pack/test/security_api_integration/plugins/saml_provider/server/init_routes.ts index ea23e04201a61..f9e84caca0531 100644 --- a/x-pack/test/security_api_integration/plugins/saml_provider/server/init_routes.ts +++ b/x-pack/test/security_api_integration/plugins/saml_provider/server/init_routes.ts @@ -5,12 +5,13 @@ * 2.0. */ -import { CoreSetup, PluginInitializerContext } from '@kbn/core/server'; +import type { CoreSetup, PluginInitializerContext } from '@kbn/core/server'; import { - getSAMLResponse, getSAMLRequestId, + getSAMLResponse, } from '@kbn/security-api-integration-helpers/saml/saml_tools'; -import { PluginSetupDependencies } from '.'; + +import type { PluginSetupDependencies } from '.'; export function initRoutes( pluginContext: PluginInitializerContext, diff --git a/x-pack/test/security_api_integration/plugins/user_profiles_consumer/server/index.ts b/x-pack/test/security_api_integration/plugins/user_profiles_consumer/server/index.ts index 3922cc233cca9..3c598e3278da1 100644 --- a/x-pack/test/security_api_integration/plugins/user_profiles_consumer/server/index.ts +++ b/x-pack/test/security_api_integration/plugins/user_profiles_consumer/server/index.ts @@ -5,10 +5,11 @@ * 2.0. */ -import type { PluginInitializer, Plugin, CoreSetup } from '@kbn/core/server'; -import { FeaturesPluginSetup, FeaturesPluginStart } from '@kbn/features-plugin/server'; -import { SecurityPluginSetup, SecurityPluginStart } from '@kbn/security-plugin/server'; -import { SpacesPluginSetup, SpacesPluginStart } from '@kbn/spaces-plugin/server'; +import type { CoreSetup, Plugin, PluginInitializer } from '@kbn/core/server'; +import type { FeaturesPluginSetup, FeaturesPluginStart } from '@kbn/features-plugin/server'; +import type { SecurityPluginSetup, SecurityPluginStart } from '@kbn/security-plugin/server'; +import type { SpacesPluginSetup, SpacesPluginStart } from '@kbn/spaces-plugin/server'; + import { initRoutes } from './init_routes'; export interface PluginSetupDependencies { diff --git a/x-pack/test/security_api_integration/plugins/user_profiles_consumer/server/init_routes.ts b/x-pack/test/security_api_integration/plugins/user_profiles_consumer/server/init_routes.ts index 091e50ff17350..d7144dd69a1df 100644 --- a/x-pack/test/security_api_integration/plugins/user_profiles_consumer/server/init_routes.ts +++ b/x-pack/test/security_api_integration/plugins/user_profiles_consumer/server/init_routes.ts @@ -5,9 +5,10 @@ * 2.0. */ -import { CoreSetup } from '@kbn/core/server'; import { schema } from '@kbn/config-schema'; -import { PluginStartDependencies } from '.'; +import type { CoreSetup } from '@kbn/core/server'; + +import type { PluginStartDependencies } from '.'; export function initRoutes(core: CoreSetup) { const router = core.http.createRouter(); diff --git a/x-pack/test/security_api_integration/saml.config.ts b/x-pack/test/security_api_integration/saml.config.ts index 3cd8b55f4117b..1168fe9ed196b 100644 --- a/x-pack/test/security_api_integration/saml.config.ts +++ b/x-pack/test/security_api_integration/saml.config.ts @@ -6,7 +6,9 @@ */ import { resolve } from 'path'; -import { FtrConfigProviderContext } from '@kbn/test'; + +import type { FtrConfigProviderContext } from '@kbn/test'; + import { services } from './services'; export default async function ({ readConfigFile }: FtrConfigProviderContext) { diff --git a/x-pack/test/security_api_integration/saml.http2.config.ts b/x-pack/test/security_api_integration/saml.http2.config.ts index 0d063188efe9c..ddcd8ac7da446 100644 --- a/x-pack/test/security_api_integration/saml.http2.config.ts +++ b/x-pack/test/security_api_integration/saml.http2.config.ts @@ -5,8 +5,8 @@ * 2.0. */ -import { FtrConfigProviderContext } from '@kbn/test'; import { CA_CERT_PATH } from '@kbn/dev-utils'; +import type { FtrConfigProviderContext } from '@kbn/test'; import { configureHTTP2 } from '@kbn/test-suites-src/common/configure_http2'; export default async function ({ readConfigFile }: FtrConfigProviderContext) { diff --git a/x-pack/test/security_api_integration/saml_cloud.config.ts b/x-pack/test/security_api_integration/saml_cloud.config.ts index ce392b8a092c1..86450379a4364 100644 --- a/x-pack/test/security_api_integration/saml_cloud.config.ts +++ b/x-pack/test/security_api_integration/saml_cloud.config.ts @@ -5,7 +5,8 @@ * 2.0. */ -import { FtrConfigProviderContext } from '@kbn/test'; +import type { FtrConfigProviderContext } from '@kbn/test'; + import { services } from './services'; export default async function ({ readConfigFile }: FtrConfigProviderContext) { diff --git a/x-pack/test/security_api_integration/services.ts b/x-pack/test/security_api_integration/services.ts index c2298b21b55e6..6b12d42f6e3d0 100644 --- a/x-pack/test/security_api_integration/services.ts +++ b/x-pack/test/security_api_integration/services.ts @@ -5,8 +5,8 @@ * 2.0. */ -import { services as commonServices } from '../common/services'; import { services as apiIntegrationServices } from '../api_integration/services'; +import { services as commonServices } from '../common/services'; export const services = { ...commonServices, diff --git a/x-pack/test/security_api_integration/session_concurrent_limit.config.ts b/x-pack/test/security_api_integration/session_concurrent_limit.config.ts index 63da7220f3959..ff3e9f590a749 100644 --- a/x-pack/test/security_api_integration/session_concurrent_limit.config.ts +++ b/x-pack/test/security_api_integration/session_concurrent_limit.config.ts @@ -6,7 +6,9 @@ */ import { resolve } from 'path'; -import { FtrConfigProviderContext } from '@kbn/test'; + +import type { FtrConfigProviderContext } from '@kbn/test'; + import { services } from './services'; // the default export of config files must be a config provider diff --git a/x-pack/test/security_api_integration/session_idle.config.ts b/x-pack/test/security_api_integration/session_idle.config.ts index a4943fb5388f4..9509d919c8ddd 100644 --- a/x-pack/test/security_api_integration/session_idle.config.ts +++ b/x-pack/test/security_api_integration/session_idle.config.ts @@ -6,7 +6,9 @@ */ import { resolve } from 'path'; -import { FtrConfigProviderContext } from '@kbn/test'; + +import type { FtrConfigProviderContext } from '@kbn/test'; + import { services } from './services'; // the default export of config files must be a config provider diff --git a/x-pack/test/security_api_integration/session_invalidate.config.ts b/x-pack/test/security_api_integration/session_invalidate.config.ts index 2e0dd37f054e0..33be8e8f8f0d2 100644 --- a/x-pack/test/security_api_integration/session_invalidate.config.ts +++ b/x-pack/test/security_api_integration/session_invalidate.config.ts @@ -6,7 +6,9 @@ */ import { resolve } from 'path'; -import { FtrConfigProviderContext } from '@kbn/test'; + +import type { FtrConfigProviderContext } from '@kbn/test'; + import { services } from './services'; // the default export of config files must be a config provider diff --git a/x-pack/test/security_api_integration/session_lifespan.config.ts b/x-pack/test/security_api_integration/session_lifespan.config.ts index 41e723a736809..9b37f45327c5c 100644 --- a/x-pack/test/security_api_integration/session_lifespan.config.ts +++ b/x-pack/test/security_api_integration/session_lifespan.config.ts @@ -6,7 +6,9 @@ */ import { resolve } from 'path'; -import { FtrConfigProviderContext } from '@kbn/test'; + +import type { FtrConfigProviderContext } from '@kbn/test'; + import { services } from './services'; // the default export of config files must be a config provider diff --git a/x-pack/test/security_api_integration/tests/anonymous/capabilities.ts b/x-pack/test/security_api_integration/tests/anonymous/capabilities.ts index baa9e9ff419a4..f4f43b070f0d5 100644 --- a/x-pack/test/security_api_integration/tests/anonymous/capabilities.ts +++ b/x-pack/test/security_api_integration/tests/anonymous/capabilities.ts @@ -5,7 +5,7 @@ * 2.0. */ -import { FtrProviderContext } from '../../ftr_provider_context'; +import type { FtrProviderContext } from '../../ftr_provider_context'; export default function ({ getService }: FtrProviderContext) { const supertest = getService('supertest'); 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 0f976589483a8..59bf0f1d75d34 100644 --- a/x-pack/test/security_api_integration/tests/anonymous/index.ts +++ b/x-pack/test/security_api_integration/tests/anonymous/index.ts @@ -5,7 +5,7 @@ * 2.0. */ -import { FtrProviderContext } from '../../ftr_provider_context'; +import type { FtrProviderContext } from '../../ftr_provider_context'; export default function ({ loadTestFile }: FtrProviderContext) { describe('security APIs - Anonymous access', function () { diff --git a/x-pack/test/security_api_integration/tests/anonymous/login.ts b/x-pack/test/security_api_integration/tests/anonymous/login.ts index b97067ba04e7d..3495bd4d71dac 100644 --- a/x-pack/test/security_api_integration/tests/anonymous/login.ts +++ b/x-pack/test/security_api_integration/tests/anonymous/login.ts @@ -5,11 +5,14 @@ * 2.0. */ +import { resolve } from 'path'; +import type { Cookie } from 'tough-cookie'; +import { parse as parseCookie } from 'tough-cookie'; + import expect from '@kbn/expect'; -import { parse as parseCookie, Cookie } from 'tough-cookie'; import { adminTestUser } from '@kbn/test'; -import { resolve } from 'path'; -import { FtrProviderContext } from '../../ftr_provider_context'; + +import type { FtrProviderContext } from '../../ftr_provider_context'; import { FileWrapper } from '../audit/file_wrapper'; export default function ({ getService }: FtrProviderContext) { diff --git a/x-pack/test/security_api_integration/tests/api_keys/grant_api_key.ts b/x-pack/test/security_api_integration/tests/api_keys/grant_api_key.ts index bc559f47ed21d..c6e69d9beff1c 100644 --- a/x-pack/test/security_api_integration/tests/api_keys/grant_api_key.ts +++ b/x-pack/test/security_api_integration/tests/api_keys/grant_api_key.ts @@ -7,7 +7,8 @@ import expect from '@kbn/expect'; import { adminTestUser } from '@kbn/test'; -import { FtrProviderContext } from '../../ftr_provider_context'; + +import type { FtrProviderContext } from '../../ftr_provider_context'; export default function ({ getService }: FtrProviderContext) { const supertest = getService('supertestWithoutAuth'); diff --git a/x-pack/test/security_api_integration/tests/api_keys/has_active_key.ts b/x-pack/test/security_api_integration/tests/api_keys/has_active_key.ts index 6b14df9ad489e..6a6c98271a888 100644 --- a/x-pack/test/security_api_integration/tests/api_keys/has_active_key.ts +++ b/x-pack/test/security_api_integration/tests/api_keys/has_active_key.ts @@ -6,6 +6,7 @@ */ import expect from '@kbn/expect'; + import type { FtrProviderContext } from '../../ftr_provider_context'; export default function ({ getService }: FtrProviderContext) { diff --git a/x-pack/test/security_api_integration/tests/api_keys/index.ts b/x-pack/test/security_api_integration/tests/api_keys/index.ts index a36a76c0c9566..11debcafe7471 100644 --- a/x-pack/test/security_api_integration/tests/api_keys/index.ts +++ b/x-pack/test/security_api_integration/tests/api_keys/index.ts @@ -5,7 +5,7 @@ * 2.0. */ -import { FtrProviderContext } from '../../ftr_provider_context'; +import type { FtrProviderContext } from '../../ftr_provider_context'; export default function ({ loadTestFile }: FtrProviderContext) { describe('security APIs - Api Keys', function () { diff --git a/x-pack/test/security_api_integration/tests/audit/audit_log.ts b/x-pack/test/security_api_integration/tests/audit/audit_log.ts index e8c5d03bfc562..083ae82d4e406 100644 --- a/x-pack/test/security_api_integration/tests/audit/audit_log.ts +++ b/x-pack/test/security_api_integration/tests/audit/audit_log.ts @@ -6,9 +6,11 @@ */ import Path from 'path'; + import expect from '@kbn/expect'; -import { FtrProviderContext } from '../../ftr_provider_context'; + import { FileWrapper } from './file_wrapper'; +import type { FtrProviderContext } from '../../ftr_provider_context'; export default function ({ getService }: FtrProviderContext) { const supertest = getService('supertest'); diff --git a/x-pack/test/security_api_integration/tests/audit/file_wrapper.ts b/x-pack/test/security_api_integration/tests/audit/file_wrapper.ts index b542ef0f8e354..2c26b07d8bd1a 100644 --- a/x-pack/test/security_api_integration/tests/audit/file_wrapper.ts +++ b/x-pack/test/security_api_integration/tests/audit/file_wrapper.ts @@ -6,6 +6,7 @@ */ import Fs from 'fs'; + import type { RetryService } from '@kbn/ftr-common-functional-services'; export class FileWrapper { 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 96b2ceb5ae3a7..aeb67988e56ff 100644 --- a/x-pack/test/security_api_integration/tests/audit/index.ts +++ b/x-pack/test/security_api_integration/tests/audit/index.ts @@ -5,7 +5,7 @@ * 2.0. */ -import { FtrProviderContext } from '../../ftr_provider_context'; +import type { FtrProviderContext } from '../../ftr_provider_context'; export default function ({ loadTestFile }: FtrProviderContext) { describe('security APIs - Audit Log', function () { diff --git a/x-pack/test/security_api_integration/tests/chips/chips_cookie.ts b/x-pack/test/security_api_integration/tests/chips/chips_cookie.ts index 9a6a811578664..753bae24ae9c7 100644 --- a/x-pack/test/security_api_integration/tests/chips/chips_cookie.ts +++ b/x-pack/test/security_api_integration/tests/chips/chips_cookie.ts @@ -13,9 +13,11 @@ */ import { parse as parseCookie } from 'tough-cookie'; -import { adminTestUser } from '@kbn/test'; + import expect from '@kbn/expect'; -import { FtrProviderContext } from '../../ftr_provider_context'; +import { adminTestUser } from '@kbn/test'; + +import type { FtrProviderContext } from '../../ftr_provider_context'; export default function ({ getService }: FtrProviderContext) { const supertest = getService('supertestWithoutAuth'); diff --git a/x-pack/test/security_api_integration/tests/chips/index.ts b/x-pack/test/security_api_integration/tests/chips/index.ts index 2379a5feae5d8..f391d6d2bc496 100644 --- a/x-pack/test/security_api_integration/tests/chips/index.ts +++ b/x-pack/test/security_api_integration/tests/chips/index.ts @@ -5,7 +5,7 @@ * 2.0. */ -import { FtrProviderContext } from '../../ftr_provider_context'; +import type { FtrProviderContext } from '../../ftr_provider_context'; export default function ({ loadTestFile }: FtrProviderContext) { describe('security APIs - CHIPS support', function () { diff --git a/x-pack/test/security_api_integration/tests/http_bearer/access_token.ts b/x-pack/test/security_api_integration/tests/http_bearer/access_token.ts index 3e94adfc3ece8..26a70bb286a1f 100644 --- a/x-pack/test/security_api_integration/tests/http_bearer/access_token.ts +++ b/x-pack/test/security_api_integration/tests/http_bearer/access_token.ts @@ -7,7 +7,8 @@ import expect from '@kbn/expect'; import { adminTestUser } from '@kbn/test'; -import { FtrProviderContext } from '../../ftr_provider_context'; + +import type { FtrProviderContext } from '../../ftr_provider_context'; export default function ({ getService }: FtrProviderContext) { const supertest = getService('supertestWithoutAuth'); 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 66619192bca48..950dcabc6b88a 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 @@ -5,7 +5,7 @@ * 2.0. */ -import { FtrProviderContext } from '../../ftr_provider_context'; +import type { FtrProviderContext } from '../../ftr_provider_context'; export default function ({ loadTestFile }: FtrProviderContext) { describe('security APIs - HTTP Bearer', function () { diff --git a/x-pack/test/security_api_integration/tests/http_bearer/jwt.ts b/x-pack/test/security_api_integration/tests/http_bearer/jwt.ts index 4806bc410cd84..2ccfdeec77cb5 100644 --- a/x-pack/test/security_api_integration/tests/http_bearer/jwt.ts +++ b/x-pack/test/security_api_integration/tests/http_bearer/jwt.ts @@ -6,9 +6,10 @@ */ import expect from '@kbn/expect'; -import { AuthenticatedUser } from '@kbn/security-plugin-types-common'; +import type { AuthenticatedUser } from '@kbn/security-plugin-types-common'; import { adminTestUser } from '@kbn/test'; -import { FtrProviderContext } from '../../ftr_provider_context'; + +import type { FtrProviderContext } from '../../ftr_provider_context'; export default function ({ getService }: FtrProviderContext) { const supertest = getService('supertestWithoutAuth'); diff --git a/x-pack/test/security_api_integration/tests/http_no_auth_providers/authentication.ts b/x-pack/test/security_api_integration/tests/http_no_auth_providers/authentication.ts index 42951f8c3fed3..c9b0c601316ba 100644 --- a/x-pack/test/security_api_integration/tests/http_no_auth_providers/authentication.ts +++ b/x-pack/test/security_api_integration/tests/http_no_auth_providers/authentication.ts @@ -7,7 +7,8 @@ import expect from '@kbn/expect'; import { adminTestUser } from '@kbn/test'; -import { FtrProviderContext } from '../../ftr_provider_context'; + +import type { FtrProviderContext } from '../../ftr_provider_context'; export default function ({ getService }: FtrProviderContext) { const supertest = getService('supertestWithoutAuth'); 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 23096b2449c9f..1ed90a8c24729 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 @@ -5,7 +5,7 @@ * 2.0. */ -import { FtrProviderContext } from '../../ftr_provider_context'; +import type { FtrProviderContext } from '../../ftr_provider_context'; export default function ({ loadTestFile }: FtrProviderContext) { describe('security APIs - HTTP no authentication providers are enabled', function () { 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 828ce7220458f..dc101b5de7c8b 100644 --- a/x-pack/test/security_api_integration/tests/kerberos/index.ts +++ b/x-pack/test/security_api_integration/tests/kerberos/index.ts @@ -5,7 +5,7 @@ * 2.0. */ -import { FtrProviderContext } from '../../ftr_provider_context'; +import type { FtrProviderContext } from '../../ftr_provider_context'; export default function ({ loadTestFile }: FtrProviderContext) { describe('security APIs - Kerberos', function () { diff --git a/x-pack/test/security_api_integration/tests/kerberos/kerberos_login.ts b/x-pack/test/security_api_integration/tests/kerberos/kerberos_login.ts index 82ec3b062405c..edfc037c936fe 100644 --- a/x-pack/test/security_api_integration/tests/kerberos/kerberos_login.ts +++ b/x-pack/test/security_api_integration/tests/kerberos/kerberos_login.ts @@ -5,17 +5,20 @@ * 2.0. */ -import expect from '@kbn/expect'; import { expect as jestExpect } from 'expect'; -import { parse as parseCookie, Cookie } from 'tough-cookie'; -import { setTimeout as setTimeoutAsync } from 'timers/promises'; -import { adminTestUser } from '@kbn/test'; import { resolve } from 'path'; +import { setTimeout as setTimeoutAsync } from 'timers/promises'; +import type { Cookie } from 'tough-cookie'; +import { parse as parseCookie } from 'tough-cookie'; + +import expect from '@kbn/expect'; import { getMutualAuthenticationResponseToken, getSPNEGOToken, } from '@kbn/security-api-integration-helpers/kerberos/kerberos_tools'; -import { FtrProviderContext } from '../../ftr_provider_context'; +import { adminTestUser } from '@kbn/test'; + +import type { FtrProviderContext } from '../../ftr_provider_context'; import { FileWrapper } from '../audit/file_wrapper'; export default function ({ getService }: FtrProviderContext) { diff --git a/x-pack/test/security_api_integration/tests/login_selector/basic_functionality.ts b/x-pack/test/security_api_integration/tests/login_selector/basic_functionality.ts index 1f2dc1ab43775..6536c9a08063d 100644 --- a/x-pack/test/security_api_integration/tests/login_selector/basic_functionality.ts +++ b/x-pack/test/security_api_integration/tests/login_selector/basic_functionality.ts @@ -5,22 +5,25 @@ * 2.0. */ -import { parse as parseCookie, Cookie } from 'tough-cookie'; import { readFileSync } from 'fs'; +import type { Cookie } from 'tough-cookie'; +import { parse as parseCookie } from 'tough-cookie'; import url from 'url'; + import { CA_CERT_PATH } from '@kbn/dev-utils'; import expect from '@kbn/expect'; -import type { AuthenticationProvider } from '@kbn/security-plugin/common'; -import { getStateAndNonce } from '@kbn/security-api-integration-helpers/oidc/oidc_tools'; import { getMutualAuthenticationResponseToken, getSPNEGOToken, } from '@kbn/security-api-integration-helpers/kerberos/kerberos_tools'; +import { getStateAndNonce } from '@kbn/security-api-integration-helpers/oidc/oidc_tools'; import { getSAMLRequestId, getSAMLResponse, } from '@kbn/security-api-integration-helpers/saml/saml_tools'; -import { FtrProviderContext } from '../../ftr_provider_context'; +import type { AuthenticationProvider } from '@kbn/security-plugin/common'; + +import type { FtrProviderContext } from '../../ftr_provider_context'; export default function ({ getService }: FtrProviderContext) { const randomness = getService('randomness'); 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 e3698340d3967..65fdef8e465e3 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 @@ -5,7 +5,7 @@ * 2.0. */ -import { FtrProviderContext } from '../../ftr_provider_context'; +import type { FtrProviderContext } from '../../ftr_provider_context'; export default function ({ loadTestFile }: FtrProviderContext) { describe('security APIs - Login Selector', function () { 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 2c8edc1569bd2..e4f9c6805c3de 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 @@ -5,7 +5,7 @@ * 2.0. */ -import { FtrProviderContext } from '../../../ftr_provider_context'; +import type { FtrProviderContext } from '../../../ftr_provider_context'; export default function ({ loadTestFile }: FtrProviderContext) { describe('security APIs - OIDC (Authorization Code Flow)', function () { diff --git a/x-pack/test/security_api_integration/tests/oidc/authorization_code_flow/oidc_auth.ts b/x-pack/test/security_api_integration/tests/oidc/authorization_code_flow/oidc_auth.ts index 4c20b091f6e11..c14379ce753af 100644 --- a/x-pack/test/security_api_integration/tests/oidc/authorization_code_flow/oidc_auth.ts +++ b/x-pack/test/security_api_integration/tests/oidc/authorization_code_flow/oidc_auth.ts @@ -5,14 +5,17 @@ * 2.0. */ -import expect from '@kbn/expect'; -import { parse as parseCookie, Cookie } from 'tough-cookie'; -import url from 'url'; -import { setTimeout as setTimeoutAsync } from 'timers/promises'; -import { adminTestUser } from '@kbn/test'; import { resolve } from 'path'; +import { setTimeout as setTimeoutAsync } from 'timers/promises'; +import type { Cookie } from 'tough-cookie'; +import { parse as parseCookie } from 'tough-cookie'; +import url from 'url'; + +import expect from '@kbn/expect'; import { getStateAndNonce } from '@kbn/security-api-integration-helpers/oidc/oidc_tools'; -import { FtrProviderContext } from '../../../ftr_provider_context'; +import { adminTestUser } from '@kbn/test'; + +import type { FtrProviderContext } from '../../../ftr_provider_context'; import { FileWrapper } from '../../audit/file_wrapper'; export default function ({ getService }: FtrProviderContext) { 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 7479ba8e7bd81..6c3215f0f34c1 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 @@ -5,7 +5,7 @@ * 2.0. */ -import { FtrProviderContext } from '../../../ftr_provider_context'; +import type { FtrProviderContext } from '../../../ftr_provider_context'; export default function ({ loadTestFile }: FtrProviderContext) { describe('security APIs - OIDC (Implicit Flow)', function () { diff --git a/x-pack/test/security_api_integration/tests/oidc/implicit_flow/oidc_auth.ts b/x-pack/test/security_api_integration/tests/oidc/implicit_flow/oidc_auth.ts index af5a78e7d4450..3a72b2ebe5fb7 100644 --- a/x-pack/test/security_api_integration/tests/oidc/implicit_flow/oidc_auth.ts +++ b/x-pack/test/security_api_integration/tests/oidc/implicit_flow/oidc_auth.ts @@ -5,15 +5,18 @@ * 2.0. */ -import expect from '@kbn/expect'; import { JSDOM } from 'jsdom'; -import { parse as parseCookie, Cookie } from 'tough-cookie'; +import type { Cookie } from 'tough-cookie'; +import { parse as parseCookie } from 'tough-cookie'; import { format as formatURL } from 'url'; + +import expect from '@kbn/expect'; import { createTokens, getStateAndNonce, } from '@kbn/security-api-integration-helpers/oidc/oidc_tools'; -import { FtrProviderContext } from '../../../ftr_provider_context'; + +import type { FtrProviderContext } from '../../../ftr_provider_context'; export default function ({ getService }: FtrProviderContext) { const supertest = getService('supertestWithoutAuth'); 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 9926f16619898..9d428f969d12e 100644 --- a/x-pack/test/security_api_integration/tests/pki/index.ts +++ b/x-pack/test/security_api_integration/tests/pki/index.ts @@ -5,7 +5,7 @@ * 2.0. */ -import { FtrProviderContext } from '../../ftr_provider_context'; +import type { FtrProviderContext } from '../../ftr_provider_context'; export default function ({ loadTestFile }: FtrProviderContext) { describe('security APIs - PKI', function () { diff --git a/x-pack/test/security_api_integration/tests/pki/pki_auth.ts b/x-pack/test/security_api_integration/tests/pki/pki_auth.ts index 37fae88942f34..bd77e029c1529 100644 --- a/x-pack/test/security_api_integration/tests/pki/pki_auth.ts +++ b/x-pack/test/security_api_integration/tests/pki/pki_auth.ts @@ -5,15 +5,18 @@ * 2.0. */ -import expect from '@kbn/expect'; import { expect as jestExpect } from 'expect'; -import { parse as parseCookie, Cookie } from 'tough-cookie'; -import { setTimeout as setTimeoutAsync } from 'timers/promises'; import { readFileSync } from 'fs'; import { resolve } from 'path'; +import { setTimeout as setTimeoutAsync } from 'timers/promises'; +import type { Cookie } from 'tough-cookie'; +import { parse as parseCookie } from 'tough-cookie'; + import { CA_CERT_PATH } from '@kbn/dev-utils'; +import expect from '@kbn/expect'; import { adminTestUser } from '@kbn/test'; -import { FtrProviderContext } from '../../ftr_provider_context'; + +import type { FtrProviderContext } from '../../ftr_provider_context'; import { FileWrapper } from '../audit/file_wrapper'; const CA_CERT = readFileSync(CA_CERT_PATH); 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 3597f1a6104ec..bacf31dd3a81a 100644 --- a/x-pack/test/security_api_integration/tests/saml/index.ts +++ b/x-pack/test/security_api_integration/tests/saml/index.ts @@ -5,7 +5,7 @@ * 2.0. */ -import { FtrProviderContext } from '../../ftr_provider_context'; +import type { FtrProviderContext } from '../../ftr_provider_context'; export default function ({ loadTestFile }: FtrProviderContext) { describe('security APIs - SAML', function () { diff --git a/x-pack/test/security_api_integration/tests/saml/saml_login.ts b/x-pack/test/security_api_integration/tests/saml/saml_login.ts index e80c19c40526d..71994bc5512ca 100644 --- a/x-pack/test/security_api_integration/tests/saml/saml_login.ts +++ b/x-pack/test/security_api_integration/tests/saml/saml_login.ts @@ -5,19 +5,22 @@ * 2.0. */ -import { stringify } from 'query-string'; -import url from 'url'; import { resolve } from 'path'; +import { stringify } from 'query-string'; import { setTimeout as setTimeoutAsync } from 'timers/promises'; +import type { Cookie } from 'tough-cookie'; +import { parse as parseCookie } from 'tough-cookie'; +import url from 'url'; + import expect from '@kbn/expect'; -import { parse as parseCookie, Cookie } from 'tough-cookie'; -import { adminTestUser } from '@kbn/test'; import { getLogoutRequest, getSAMLRequestId, getSAMLResponse, } from '@kbn/security-api-integration-helpers/saml/saml_tools'; -import { FtrProviderContext } from '../../ftr_provider_context'; +import { adminTestUser } from '@kbn/test'; + +import type { FtrProviderContext } from '../../ftr_provider_context'; import { FileWrapper } from '../audit/file_wrapper'; export default function ({ getService }: FtrProviderContext) { diff --git a/x-pack/test/security_api_integration/tests/saml_cloud/index.ts b/x-pack/test/security_api_integration/tests/saml_cloud/index.ts index a2b39ad247151..61b4a166f666c 100644 --- a/x-pack/test/security_api_integration/tests/saml_cloud/index.ts +++ b/x-pack/test/security_api_integration/tests/saml_cloud/index.ts @@ -5,7 +5,7 @@ * 2.0. */ -import { FtrProviderContext } from '../../ftr_provider_context'; +import type { FtrProviderContext } from '../../ftr_provider_context'; export default function ({ loadTestFile }: FtrProviderContext) { describe('security APIs - Cloud SAML', function () { diff --git a/x-pack/test/security_api_integration/tests/saml_cloud/saml_login.ts b/x-pack/test/security_api_integration/tests/saml_cloud/saml_login.ts index 7c585fcf4ffae..1282654506300 100644 --- a/x-pack/test/security_api_integration/tests/saml_cloud/saml_login.ts +++ b/x-pack/test/security_api_integration/tests/saml_cloud/saml_login.ts @@ -5,10 +5,13 @@ * 2.0. */ +import type { Cookie } from 'tough-cookie'; +import { parse as parseCookie } from 'tough-cookie'; + import expect from '@kbn/expect'; -import { parse as parseCookie, Cookie } from 'tough-cookie'; import { getSAMLResponse } from '@kbn/security-api-integration-helpers/saml/saml_tools'; -import { FtrProviderContext } from '../../ftr_provider_context'; + +import type { FtrProviderContext } from '../../ftr_provider_context'; export default function ({ getService }: FtrProviderContext) { const randomness = getService('randomness'); diff --git a/x-pack/test/security_api_integration/tests/session_concurrent_limit/cleanup.ts b/x-pack/test/security_api_integration/tests/session_concurrent_limit/cleanup.ts index 6aa782ad260df..cac4f3b678149 100644 --- a/x-pack/test/security_api_integration/tests/session_concurrent_limit/cleanup.ts +++ b/x-pack/test/security_api_integration/tests/session_concurrent_limit/cleanup.ts @@ -5,23 +5,26 @@ * 2.0. */ -import { parse as parseCookie, Cookie } from 'tough-cookie'; -import { setTimeout as setTimeoutAsync } from 'timers/promises'; -import expect from '@kbn/expect'; -import { adminTestUser } from '@kbn/test'; -import type { AuthenticationProvider } from '@kbn/security-plugin/common'; -import { +import type { AggregateName, AggregationsMultiTermsAggregate, AggregationsMultiTermsBucket, AggregationsTopHitsAggregate, SearchTotalHits, } from '@elastic/elasticsearch/lib/api/types'; +import { setTimeout as setTimeoutAsync } from 'timers/promises'; +import type { Cookie } from 'tough-cookie'; +import { parse as parseCookie } from 'tough-cookie'; + +import expect from '@kbn/expect'; import { getSAMLRequestId, getSAMLResponse, } from '@kbn/security-api-integration-helpers/saml/saml_tools'; -import { FtrProviderContext } from '../../ftr_provider_context'; +import type { AuthenticationProvider } from '@kbn/security-plugin/common'; +import { adminTestUser } from '@kbn/test'; + +import type { FtrProviderContext } from '../../ftr_provider_context'; export default function ({ getService }: FtrProviderContext) { const supertest = getService('supertestWithoutAuth'); diff --git a/x-pack/test/security_api_integration/tests/session_concurrent_limit/global_limit.ts b/x-pack/test/security_api_integration/tests/session_concurrent_limit/global_limit.ts index df120e033377c..b1bb30e1a6cb6 100644 --- a/x-pack/test/security_api_integration/tests/session_concurrent_limit/global_limit.ts +++ b/x-pack/test/security_api_integration/tests/session_concurrent_limit/global_limit.ts @@ -5,15 +5,18 @@ * 2.0. */ -import { parse as parseCookie, Cookie } from 'tough-cookie'; +import type { Cookie } from 'tough-cookie'; +import { parse as parseCookie } from 'tough-cookie'; + import expect from '@kbn/expect'; -import { adminTestUser } from '@kbn/test'; -import type { AuthenticationProvider } from '@kbn/security-plugin/common'; import { getSAMLRequestId, getSAMLResponse, } from '@kbn/security-api-integration-helpers/saml/saml_tools'; -import { FtrProviderContext } from '../../ftr_provider_context'; +import type { AuthenticationProvider } from '@kbn/security-plugin/common'; +import { adminTestUser } from '@kbn/test'; + +import type { FtrProviderContext } from '../../ftr_provider_context'; export default function ({ getService }: FtrProviderContext) { const supertest = getService('supertestWithoutAuth'); diff --git a/x-pack/test/security_api_integration/tests/session_concurrent_limit/index.ts b/x-pack/test/security_api_integration/tests/session_concurrent_limit/index.ts index 61463eaccb09b..ce4b0f61a708f 100644 --- a/x-pack/test/security_api_integration/tests/session_concurrent_limit/index.ts +++ b/x-pack/test/security_api_integration/tests/session_concurrent_limit/index.ts @@ -5,7 +5,7 @@ * 2.0. */ -import { FtrProviderContext } from '../../ftr_provider_context'; +import type { FtrProviderContext } from '../../ftr_provider_context'; export default function ({ loadTestFile }: FtrProviderContext) { describe('security APIs - Session Concurrent Limit', function () { diff --git a/x-pack/test/security_api_integration/tests/session_idle/cleanup.ts b/x-pack/test/security_api_integration/tests/session_idle/cleanup.ts index 4a74e3938467e..9c0f92b2c0182 100644 --- a/x-pack/test/security_api_integration/tests/session_idle/cleanup.ts +++ b/x-pack/test/security_api_integration/tests/session_idle/cleanup.ts @@ -5,16 +5,19 @@ * 2.0. */ -import { parse as parseCookie, Cookie } from 'tough-cookie'; import { setTimeout as setTimeoutAsync } from 'timers/promises'; +import type { Cookie } from 'tough-cookie'; +import { parse as parseCookie } from 'tough-cookie'; + import expect from '@kbn/expect'; -import { adminTestUser } from '@kbn/test'; -import type { AuthenticationProvider } from '@kbn/security-plugin/common'; import { getSAMLRequestId, getSAMLResponse, } from '@kbn/security-api-integration-helpers/saml/saml_tools'; -import { FtrProviderContext } from '../../ftr_provider_context'; +import type { AuthenticationProvider } from '@kbn/security-plugin/common'; +import { adminTestUser } from '@kbn/test'; + +import type { FtrProviderContext } from '../../ftr_provider_context'; export default function ({ getService }: FtrProviderContext) { const supertest = getService('supertestWithoutAuth'); diff --git a/x-pack/test/security_api_integration/tests/session_idle/expired.ts b/x-pack/test/security_api_integration/tests/session_idle/expired.ts index 14c7370068634..f5eb553a462ff 100644 --- a/x-pack/test/security_api_integration/tests/session_idle/expired.ts +++ b/x-pack/test/security_api_integration/tests/session_idle/expired.ts @@ -5,12 +5,14 @@ * 2.0. */ -import { parse as parseCookie } from 'tough-cookie'; import { setTimeout as setTimeoutAsync } from 'timers/promises'; +import { parse as parseCookie } from 'tough-cookie'; + import expect from '@kbn/expect'; -import { adminTestUser } from '@kbn/test'; import { SESSION_ERROR_REASON_HEADER } from '@kbn/security-plugin/common/constants'; -import { FtrProviderContext } from '../../ftr_provider_context'; +import { adminTestUser } from '@kbn/test'; + +import type { FtrProviderContext } from '../../ftr_provider_context'; export default function ({ getService }: FtrProviderContext) { const supertest = getService('supertestWithoutAuth'); diff --git a/x-pack/test/security_api_integration/tests/session_idle/extension.ts b/x-pack/test/security_api_integration/tests/session_idle/extension.ts index 83a3d1a534919..71e1c370387de 100644 --- a/x-pack/test/security_api_integration/tests/session_idle/extension.ts +++ b/x-pack/test/security_api_integration/tests/session_idle/extension.ts @@ -5,10 +5,13 @@ * 2.0. */ -import { parse as parseCookie, Cookie } from 'tough-cookie'; -import expect from '@kbn/expect'; import { setTimeout as setTimeoutAsync } from 'timers/promises'; -import { FtrProviderContext } from '../../ftr_provider_context'; +import type { Cookie } from 'tough-cookie'; +import { parse as parseCookie } from 'tough-cookie'; + +import expect from '@kbn/expect'; + +import type { FtrProviderContext } from '../../ftr_provider_context'; export default function ({ getService }: FtrProviderContext) { const supertestWithoutAuth = getService('supertestWithoutAuth'); 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 14015874ddb61..d23861046ba59 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 @@ -5,7 +5,7 @@ * 2.0. */ -import { FtrProviderContext } from '../../ftr_provider_context'; +import type { FtrProviderContext } from '../../ftr_provider_context'; export default function ({ loadTestFile }: FtrProviderContext) { describe('security APIs - Session Idle', function () { 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 dcfb3d7fc5259..e244b08d03719 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 @@ -5,7 +5,7 @@ * 2.0. */ -import { FtrProviderContext } from '../../ftr_provider_context'; +import type { FtrProviderContext } from '../../ftr_provider_context'; export default function ({ loadTestFile }: FtrProviderContext) { describe('security APIs - Session Invalidate', function () { diff --git a/x-pack/test/security_api_integration/tests/session_invalidate/invalidate.ts b/x-pack/test/security_api_integration/tests/session_invalidate/invalidate.ts index 7c4d344a481d0..184b46a7d13c2 100644 --- a/x-pack/test/security_api_integration/tests/session_invalidate/invalidate.ts +++ b/x-pack/test/security_api_integration/tests/session_invalidate/invalidate.ts @@ -5,15 +5,18 @@ * 2.0. */ -import { parse as parseCookie, Cookie } from 'tough-cookie'; +import type { Cookie } from 'tough-cookie'; +import { parse as parseCookie } from 'tough-cookie'; + import expect from '@kbn/expect'; -import { adminTestUser } from '@kbn/test'; -import type { AuthenticationProvider } from '@kbn/security-plugin/common'; import { getSAMLRequestId, getSAMLResponse, } from '@kbn/security-api-integration-helpers/saml/saml_tools'; -import { FtrProviderContext } from '../../ftr_provider_context'; +import type { AuthenticationProvider } from '@kbn/security-plugin/common'; +import { adminTestUser } from '@kbn/test'; + +import type { FtrProviderContext } from '../../ftr_provider_context'; export default function ({ getService }: FtrProviderContext) { const supertest = getService('supertestWithoutAuth'); diff --git a/x-pack/test/security_api_integration/tests/session_lifespan/cleanup.ts b/x-pack/test/security_api_integration/tests/session_lifespan/cleanup.ts index 5f7efaefd6242..77de7d5915a45 100644 --- a/x-pack/test/security_api_integration/tests/session_lifespan/cleanup.ts +++ b/x-pack/test/security_api_integration/tests/session_lifespan/cleanup.ts @@ -5,16 +5,19 @@ * 2.0. */ -import { parse as parseCookie, Cookie } from 'tough-cookie'; import { setTimeout as setTimeoutAsync } from 'timers/promises'; +import type { Cookie } from 'tough-cookie'; +import { parse as parseCookie } from 'tough-cookie'; + import expect from '@kbn/expect'; -import { adminTestUser } from '@kbn/test'; -import type { AuthenticationProvider } from '@kbn/security-plugin/common'; import { getSAMLRequestId, getSAMLResponse, } from '@kbn/security-api-integration-helpers/saml/saml_tools'; -import { FtrProviderContext } from '../../ftr_provider_context'; +import type { AuthenticationProvider } from '@kbn/security-plugin/common'; +import { adminTestUser } from '@kbn/test'; + +import type { FtrProviderContext } from '../../ftr_provider_context'; export default function ({ getService }: FtrProviderContext) { const supertest = getService('supertestWithoutAuth'); 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 e297805b4ab3c..e8f96c7f83f98 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 @@ -5,7 +5,7 @@ * 2.0. */ -import { FtrProviderContext } from '../../ftr_provider_context'; +import type { FtrProviderContext } from '../../ftr_provider_context'; export default function ({ loadTestFile }: FtrProviderContext) { describe('security APIs - Session Lifespan', function () { diff --git a/x-pack/test/security_api_integration/tests/token/audit.ts b/x-pack/test/security_api_integration/tests/token/audit.ts index 0c97fb9c3cdc4..7bd32b733b175 100644 --- a/x-pack/test/security_api_integration/tests/token/audit.ts +++ b/x-pack/test/security_api_integration/tests/token/audit.ts @@ -5,11 +5,12 @@ * 2.0. */ -import { parse as parseCookie } from 'tough-cookie'; import { resolve } from 'path'; +import { parse as parseCookie } from 'tough-cookie'; + import expect from '@kbn/expect'; -import { FtrProviderContext } from '../../ftr_provider_context'; +import type { FtrProviderContext } from '../../ftr_provider_context'; import { FileWrapper } from '../audit/file_wrapper'; export default function ({ getService }: FtrProviderContext) { diff --git a/x-pack/test/security_api_integration/tests/token/header.ts b/x-pack/test/security_api_integration/tests/token/header.ts index 74707aee68931..304d2f4b0e4dd 100644 --- a/x-pack/test/security_api_integration/tests/token/header.ts +++ b/x-pack/test/security_api_integration/tests/token/header.ts @@ -5,7 +5,7 @@ * 2.0. */ -import { FtrProviderContext } from '../../ftr_provider_context'; +import type { FtrProviderContext } from '../../ftr_provider_context'; export default function ({ getService }: FtrProviderContext) { const supertest = getService('supertestWithoutAuth'); 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 e38f5148d644d..d4ed1589370b4 100644 --- a/x-pack/test/security_api_integration/tests/token/index.ts +++ b/x-pack/test/security_api_integration/tests/token/index.ts @@ -5,7 +5,7 @@ * 2.0. */ -import { FtrProviderContext } from '../../ftr_provider_context'; +import type { FtrProviderContext } from '../../ftr_provider_context'; export default function ({ loadTestFile }: FtrProviderContext) { describe('security APIs - Token', function () { diff --git a/x-pack/test/security_api_integration/tests/token/login.ts b/x-pack/test/security_api_integration/tests/token/login.ts index 25e7bb3251687..a05e78c1c1309 100644 --- a/x-pack/test/security_api_integration/tests/token/login.ts +++ b/x-pack/test/security_api_integration/tests/token/login.ts @@ -6,7 +6,8 @@ */ import { parse as parseCookie } from 'tough-cookie'; -import { FtrProviderContext } from '../../ftr_provider_context'; + +import type { FtrProviderContext } from '../../ftr_provider_context'; export default function ({ getService }: FtrProviderContext) { const supertest = getService('supertestWithoutAuth'); diff --git a/x-pack/test/security_api_integration/tests/token/logout.ts b/x-pack/test/security_api_integration/tests/token/logout.ts index 1a2385e434ca4..5c85f22964089 100644 --- a/x-pack/test/security_api_integration/tests/token/logout.ts +++ b/x-pack/test/security_api_integration/tests/token/logout.ts @@ -6,7 +6,8 @@ */ import { parse as parseCookie } from 'tough-cookie'; -import { FtrProviderContext } from '../../ftr_provider_context'; + +import type { FtrProviderContext } from '../../ftr_provider_context'; export default function ({ getService }: FtrProviderContext) { const supertest = getService('supertestWithoutAuth'); diff --git a/x-pack/test/security_api_integration/tests/token/session.ts b/x-pack/test/security_api_integration/tests/token/session.ts index 3e8d44ce247cc..1e90f278ea594 100644 --- a/x-pack/test/security_api_integration/tests/token/session.ts +++ b/x-pack/test/security_api_integration/tests/token/session.ts @@ -5,9 +5,12 @@ * 2.0. */ -import { parse as parseCookie, Cookie } from 'tough-cookie'; +import type { Cookie } from 'tough-cookie'; +import { parse as parseCookie } from 'tough-cookie'; + import expect from '@kbn/expect'; -import { FtrProviderContext } from '../../ftr_provider_context'; + +import type { FtrProviderContext } from '../../ftr_provider_context'; const delay = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms)); diff --git a/x-pack/test/security_api_integration/tests/user_profiles/bulk_get.ts b/x-pack/test/security_api_integration/tests/user_profiles/bulk_get.ts index 8955a06261848..7c5accb39937c 100644 --- a/x-pack/test/security_api_integration/tests/user_profiles/bulk_get.ts +++ b/x-pack/test/security_api_integration/tests/user_profiles/bulk_get.ts @@ -5,9 +5,12 @@ * 2.0. */ +import type { Cookie } from 'tough-cookie'; +import { parse as parseCookie } from 'tough-cookie'; + import expect from '@kbn/expect'; -import { parse as parseCookie, Cookie } from 'tough-cookie'; -import { FtrProviderContext } from '../../ftr_provider_context'; + +import type { FtrProviderContext } from '../../ftr_provider_context'; export default function ({ getService }: FtrProviderContext) { const supertest = getService('supertest'); diff --git a/x-pack/test/security_api_integration/tests/user_profiles/get_current.ts b/x-pack/test/security_api_integration/tests/user_profiles/get_current.ts index 44896f9792247..ec41bc3d80648 100644 --- a/x-pack/test/security_api_integration/tests/user_profiles/get_current.ts +++ b/x-pack/test/security_api_integration/tests/user_profiles/get_current.ts @@ -5,9 +5,10 @@ * 2.0. */ -import { parse as parseCookie } from 'tough-cookie'; import { expect } from 'expect'; -import { FtrProviderContext } from '../../ftr_provider_context'; +import { parse as parseCookie } from 'tough-cookie'; + +import type { FtrProviderContext } from '../../ftr_provider_context'; export default function ({ getService }: FtrProviderContext) { const supertestWithoutAuth = getService('supertestWithoutAuth'); diff --git a/x-pack/test/security_api_integration/tests/user_profiles/index.ts b/x-pack/test/security_api_integration/tests/user_profiles/index.ts index ab91926127f41..19197cfebbbcb 100644 --- a/x-pack/test/security_api_integration/tests/user_profiles/index.ts +++ b/x-pack/test/security_api_integration/tests/user_profiles/index.ts @@ -5,7 +5,7 @@ * 2.0. */ -import { FtrProviderContext } from '../../ftr_provider_context'; +import type { FtrProviderContext } from '../../ftr_provider_context'; export default function ({ loadTestFile }: FtrProviderContext) { describe('security APIs - User Profiles', function () { diff --git a/x-pack/test/security_api_integration/tests/user_profiles/suggest.ts b/x-pack/test/security_api_integration/tests/user_profiles/suggest.ts index cf58f1b35d3b5..26b60abfc04d3 100644 --- a/x-pack/test/security_api_integration/tests/user_profiles/suggest.ts +++ b/x-pack/test/security_api_integration/tests/user_profiles/suggest.ts @@ -5,9 +5,12 @@ * 2.0. */ +import type { Cookie } from 'tough-cookie'; +import { parse as parseCookie } from 'tough-cookie'; + import expect from '@kbn/expect'; -import { parse as parseCookie, Cookie } from 'tough-cookie'; -import { FtrProviderContext } from '../../ftr_provider_context'; + +import type { FtrProviderContext } from '../../ftr_provider_context'; export default function ({ getService }: FtrProviderContext) { const supertest = getService('supertest'); diff --git a/x-pack/test/security_api_integration/token.config.ts b/x-pack/test/security_api_integration/token.config.ts index 9021ab8dc2a58..5eefa10dc7621 100644 --- a/x-pack/test/security_api_integration/token.config.ts +++ b/x-pack/test/security_api_integration/token.config.ts @@ -5,8 +5,10 @@ * 2.0. */ -import { FtrConfigProviderContext } from '@kbn/test'; import { resolve } from 'path'; + +import type { FtrConfigProviderContext } from '@kbn/test'; + import { services } from './services'; export default async function ({ readConfigFile }: FtrConfigProviderContext) { diff --git a/x-pack/test/security_api_integration/user_profiles.config.ts b/x-pack/test/security_api_integration/user_profiles.config.ts index 43d25920b2fd9..31b548e2dc3e6 100644 --- a/x-pack/test/security_api_integration/user_profiles.config.ts +++ b/x-pack/test/security_api_integration/user_profiles.config.ts @@ -6,7 +6,9 @@ */ import { resolve } from 'path'; -import { FtrConfigProviderContext } from '@kbn/test'; + +import type { FtrConfigProviderContext } from '@kbn/test'; + import { services } from './services'; export default async function ({ readConfigFile }: FtrConfigProviderContext) { diff --git a/x-pack/test/security_functional/expired_session.config.ts b/x-pack/test/security_functional/expired_session.config.ts index 82dd1cb36caf2..b02a96dd41360 100644 --- a/x-pack/test/security_functional/expired_session.config.ts +++ b/x-pack/test/security_functional/expired_session.config.ts @@ -6,9 +6,11 @@ */ import { resolve } from 'path'; -import { FtrConfigProviderContext } from '@kbn/test'; -import { services } from '../functional/services'; + +import type { FtrConfigProviderContext } from '@kbn/test'; + import { pageObjects } from '../functional/page_objects'; +import { services } from '../functional/services'; // the default export of config files must be a config provider // that returns an object with the projects config values diff --git a/x-pack/test/security_functional/ftr_provider_context.d.ts b/x-pack/test/security_functional/ftr_provider_context.d.ts index 66d4e37b795ca..87ecf00ddae2d 100644 --- a/x-pack/test/security_functional/ftr_provider_context.d.ts +++ b/x-pack/test/security_functional/ftr_provider_context.d.ts @@ -5,9 +5,9 @@ * 2.0. */ -import { GenericFtrProviderContext } from '@kbn/test'; +import type { GenericFtrProviderContext } from '@kbn/test'; -import { pageObjects } from '../functional/page_objects'; -import { services } from '../functional/services'; +import type { pageObjects } from '../functional/page_objects'; +import type { services } from '../functional/services'; export type FtrProviderContext = GenericFtrProviderContext; diff --git a/x-pack/test/security_functional/insecure_cluster_warning.config.ts b/x-pack/test/security_functional/insecure_cluster_warning.config.ts index fa89c80653c5d..4a43c9f928f0f 100644 --- a/x-pack/test/security_functional/insecure_cluster_warning.config.ts +++ b/x-pack/test/security_functional/insecure_cluster_warning.config.ts @@ -6,9 +6,11 @@ */ import { resolve } from 'path'; -import { FtrConfigProviderContext } from '@kbn/test'; -import { services } from '../functional/services'; + +import type { FtrConfigProviderContext } from '@kbn/test'; + import { pageObjects } from '../functional/page_objects'; +import { services } from '../functional/services'; // the default export of config files must be a config provider // that returns an object with the projects config values diff --git a/x-pack/test/security_functional/login_selector.config.ts b/x-pack/test/security_functional/login_selector.config.ts index 2a35fc77a0316..d6abf075281f0 100644 --- a/x-pack/test/security_functional/login_selector.config.ts +++ b/x-pack/test/security_functional/login_selector.config.ts @@ -6,9 +6,11 @@ */ import { resolve } from 'path'; -import { FtrConfigProviderContext } from '@kbn/test'; -import { services } from '../functional/services'; + +import type { FtrConfigProviderContext } from '@kbn/test'; + import { pageObjects } from '../functional/page_objects'; +import { services } from '../functional/services'; // the default export of config files must be a config provider // that returns an object with the projects config values diff --git a/x-pack/test/security_functional/oidc.config.ts b/x-pack/test/security_functional/oidc.config.ts index a50975c0ee7bb..4773d2e81497f 100644 --- a/x-pack/test/security_functional/oidc.config.ts +++ b/x-pack/test/security_functional/oidc.config.ts @@ -6,9 +6,11 @@ */ import { resolve } from 'path'; -import { FtrConfigProviderContext } from '@kbn/test'; -import { services } from '../functional/services'; + +import type { FtrConfigProviderContext } from '@kbn/test'; + import { pageObjects } from '../functional/page_objects'; +import { services } from '../functional/services'; // the default export of config files must be a config provider // that returns an object with the projects config values diff --git a/x-pack/test/security_functional/oidc.http2.config.ts b/x-pack/test/security_functional/oidc.http2.config.ts index 79335988411f2..fbba56727f284 100644 --- a/x-pack/test/security_functional/oidc.http2.config.ts +++ b/x-pack/test/security_functional/oidc.http2.config.ts @@ -5,8 +5,8 @@ * 2.0. */ -import { FtrConfigProviderContext } from '@kbn/test'; import { CA_CERT_PATH } from '@kbn/dev-utils'; +import type { FtrConfigProviderContext } from '@kbn/test'; import { configureHTTP2 } from '@kbn/test-suites-src/common/configure_http2'; export default async function ({ readConfigFile }: FtrConfigProviderContext) { diff --git a/x-pack/test/security_functional/plugins/test_endpoints/public/plugin.tsx b/x-pack/test/security_functional/plugins/test_endpoints/public/plugin.tsx index 716187792615d..a8f0be086fe06 100644 --- a/x-pack/test/security_functional/plugins/test_endpoints/public/plugin.tsx +++ b/x-pack/test/security_functional/plugins/test_endpoints/public/plugin.tsx @@ -5,12 +5,12 @@ * 2.0. */ -import type { CoreSetup, Plugin } from '@kbn/core/public'; -import ReactDOM from 'react-dom'; import React from 'react'; -import { debounce, filter, first } from 'rxjs'; -import { timer } from 'rxjs'; -import { SecurityPluginStart } from '@kbn/security-plugin/public'; +import ReactDOM from 'react-dom'; +import { debounce, filter, first, timer } from 'rxjs'; + +import type { CoreSetup, Plugin } from '@kbn/core/public'; +import type { SecurityPluginStart } from '@kbn/security-plugin/public'; export interface PluginStartDependencies { security: SecurityPluginStart; diff --git a/x-pack/test/security_functional/plugins/test_endpoints/server/index.ts b/x-pack/test/security_functional/plugins/test_endpoints/server/index.ts index 93097b9fa712e..178531cd3e6c2 100644 --- a/x-pack/test/security_functional/plugins/test_endpoints/server/index.ts +++ b/x-pack/test/security_functional/plugins/test_endpoints/server/index.ts @@ -5,12 +5,13 @@ * 2.0. */ -import { PluginInitializer, Plugin, CoreSetup } from '@kbn/core/server'; -import { +import type { CoreSetup, Plugin, PluginInitializer } from '@kbn/core/server'; +import type { SecurityPluginSetup, SecurityPluginStart } from '@kbn/security-plugin/server'; +import type { TaskManagerSetupContract, TaskManagerStartContract, } from '@kbn/task-manager-plugin/server'; -import { SecurityPluginSetup, SecurityPluginStart } from '@kbn/security-plugin/server'; + import { initRoutes } from './init_routes'; export interface PluginSetupDependencies { diff --git a/x-pack/test/security_functional/plugins/test_endpoints/server/init_routes.ts b/x-pack/test/security_functional/plugins/test_endpoints/server/init_routes.ts index 70a077305ba39..23fb20c02e42a 100644 --- a/x-pack/test/security_functional/plugins/test_endpoints/server/init_routes.ts +++ b/x-pack/test/security_functional/plugins/test_endpoints/server/init_routes.ts @@ -5,17 +5,19 @@ * 2.0. */ -import { schema } from '@kbn/config-schema'; import { errors } from '@elastic/elasticsearch'; -import { CoreSetup, CoreStart, PluginInitializerContext } from '@kbn/core/server'; + +import { schema } from '@kbn/config-schema'; +import type { CoreSetup, CoreStart, PluginInitializerContext } from '@kbn/core/server'; +import { ROUTE_TAG_AUTH_FLOW } from '@kbn/security-plugin/server'; +import { restApiKeySchema } from '@kbn/security-plugin-types-server'; import type { - TaskManagerStartContract, - ConcreteTaskInstance, BulkUpdateTaskResult, + ConcreteTaskInstance, + TaskManagerStartContract, } from '@kbn/task-manager-plugin/server'; -import { restApiKeySchema } from '@kbn/security-plugin-types-server'; -import { ROUTE_TAG_AUTH_FLOW } from '@kbn/security-plugin/server'; -import { PluginStartDependencies } from '.'; + +import type { PluginStartDependencies } from '.'; export const SESSION_INDEX_CLEANUP_TASK_NAME = 'session_cleanup'; diff --git a/x-pack/test/security_functional/saml.config.ts b/x-pack/test/security_functional/saml.config.ts index 52ac62336c681..91afd72c97747 100644 --- a/x-pack/test/security_functional/saml.config.ts +++ b/x-pack/test/security_functional/saml.config.ts @@ -6,9 +6,11 @@ */ import { resolve } from 'path'; -import { FtrConfigProviderContext } from '@kbn/test'; -import { services } from '../functional/services'; + +import type { FtrConfigProviderContext } from '@kbn/test'; + import { pageObjects } from '../functional/page_objects'; +import { services } from '../functional/services'; // the default export of config files must be a config provider // that returns an object with the projects config values diff --git a/x-pack/test/security_functional/saml.http2.config.ts b/x-pack/test/security_functional/saml.http2.config.ts index dc9bac7cbdeb9..34131c82e0090 100644 --- a/x-pack/test/security_functional/saml.http2.config.ts +++ b/x-pack/test/security_functional/saml.http2.config.ts @@ -12,8 +12,9 @@ */ import { resolve } from 'path'; -import { FtrConfigProviderContext } from '@kbn/test'; + import { CA_CERT_PATH } from '@kbn/dev-utils'; +import type { FtrConfigProviderContext } from '@kbn/test'; import { configureHTTP2 } from '@kbn/test-suites-src/common/configure_http2'; // the default export of config files must be a config provider diff --git a/x-pack/test/security_functional/tests/expired_session/basic_functionality.ts b/x-pack/test/security_functional/tests/expired_session/basic_functionality.ts index bdefbe94c9b22..7997ad5507280 100644 --- a/x-pack/test/security_functional/tests/expired_session/basic_functionality.ts +++ b/x-pack/test/security_functional/tests/expired_session/basic_functionality.ts @@ -5,11 +5,13 @@ * 2.0. */ -import expect from '@kbn/expect'; -import { SESSION_ERROR_REASON_HEADER } from '@kbn/security-plugin/common/constants'; import { setTimeout as setTimeoutAsync } from 'timers/promises'; import { parse } from 'url'; -import { FtrProviderContext } from '../../ftr_provider_context'; + +import expect from '@kbn/expect'; +import { SESSION_ERROR_REASON_HEADER } from '@kbn/security-plugin/common/constants'; + +import type { FtrProviderContext } from '../../ftr_provider_context'; export default function ({ getService, getPageObjects }: FtrProviderContext) { const browser = getService('browser'); diff --git a/x-pack/test/security_functional/tests/expired_session/index.ts b/x-pack/test/security_functional/tests/expired_session/index.ts index d2d741b2b8393..15f6c6ad508e6 100644 --- a/x-pack/test/security_functional/tests/expired_session/index.ts +++ b/x-pack/test/security_functional/tests/expired_session/index.ts @@ -5,7 +5,7 @@ * 2.0. */ -import { FtrProviderContext } from '../../ftr_provider_context'; +import type { FtrProviderContext } from '../../ftr_provider_context'; export default function ({ loadTestFile }: FtrProviderContext) { describe('security app - expired session', function () { diff --git a/x-pack/test/security_functional/tests/insecure_cluster_warning/index.ts b/x-pack/test/security_functional/tests/insecure_cluster_warning/index.ts index 5878deb83c6a4..e737cf91f1c20 100644 --- a/x-pack/test/security_functional/tests/insecure_cluster_warning/index.ts +++ b/x-pack/test/security_functional/tests/insecure_cluster_warning/index.ts @@ -5,7 +5,7 @@ * 2.0. */ -import { FtrProviderContext } from '../../ftr_provider_context'; +import type { FtrProviderContext } from '../../ftr_provider_context'; export default function ({ loadTestFile }: FtrProviderContext) { describe('security app - insecure cluster warning', function () { diff --git a/x-pack/test/security_functional/tests/insecure_cluster_warning/insecure_cluster_warning.ts b/x-pack/test/security_functional/tests/insecure_cluster_warning/insecure_cluster_warning.ts index 32640b653a491..9306b290a0ec1 100644 --- a/x-pack/test/security_functional/tests/insecure_cluster_warning/insecure_cluster_warning.ts +++ b/x-pack/test/security_functional/tests/insecure_cluster_warning/insecure_cluster_warning.ts @@ -6,7 +6,8 @@ */ import expect from '@kbn/expect'; -import { FtrProviderContext } from '../../ftr_provider_context'; + +import type { FtrProviderContext } from '../../ftr_provider_context'; export default function ({ getService, getPageObjects }: FtrProviderContext) { const PageObjects = getPageObjects(['common']); diff --git a/x-pack/test/security_functional/tests/login_selector/auth_provider_hint.ts b/x-pack/test/security_functional/tests/login_selector/auth_provider_hint.ts index d99b17d69f461..bcb41c609e512 100644 --- a/x-pack/test/security_functional/tests/login_selector/auth_provider_hint.ts +++ b/x-pack/test/security_functional/tests/login_selector/auth_provider_hint.ts @@ -5,9 +5,11 @@ * 2.0. */ -import expect from '@kbn/expect'; import { parse } from 'url'; -import { FtrProviderContext } from '../../ftr_provider_context'; + +import expect from '@kbn/expect'; + +import type { FtrProviderContext } from '../../ftr_provider_context'; export default function ({ getService, getPageObjects }: FtrProviderContext) { const browser = getService('browser'); diff --git a/x-pack/test/security_functional/tests/login_selector/basic_functionality.ts b/x-pack/test/security_functional/tests/login_selector/basic_functionality.ts index fc808b4c33cbd..49bc6038d88c8 100644 --- a/x-pack/test/security_functional/tests/login_selector/basic_functionality.ts +++ b/x-pack/test/security_functional/tests/login_selector/basic_functionality.ts @@ -5,9 +5,11 @@ * 2.0. */ -import expect from '@kbn/expect'; import { parse } from 'url'; -import { FtrProviderContext } from '../../ftr_provider_context'; + +import expect from '@kbn/expect'; + +import type { FtrProviderContext } from '../../ftr_provider_context'; export default function ({ getService, getPageObjects }: FtrProviderContext) { const testSubjects = getService('testSubjects'); 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 ae81b307987cc..584d5410feb2d 100644 --- a/x-pack/test/security_functional/tests/login_selector/index.ts +++ b/x-pack/test/security_functional/tests/login_selector/index.ts @@ -5,7 +5,7 @@ * 2.0. */ -import { FtrProviderContext } from '../../ftr_provider_context'; +import type { FtrProviderContext } from '../../ftr_provider_context'; export default function ({ loadTestFile }: FtrProviderContext) { describe('security app - login selector', function () { diff --git a/x-pack/test/security_functional/tests/login_selector/reset_session_page.ts b/x-pack/test/security_functional/tests/login_selector/reset_session_page.ts index 4d7eb666a4bc3..0dd935f86be82 100644 --- a/x-pack/test/security_functional/tests/login_selector/reset_session_page.ts +++ b/x-pack/test/security_functional/tests/login_selector/reset_session_page.ts @@ -6,7 +6,8 @@ */ import expect from '@kbn/expect'; -import { FtrProviderContext } from '../../ftr_provider_context'; + +import type { FtrProviderContext } from '../../ftr_provider_context'; export default function ({ getService, getPageObjects, updateBaselines }: FtrProviderContext) { const screenshots = getService('screenshots'); diff --git a/x-pack/test/security_functional/tests/oidc/index.ts b/x-pack/test/security_functional/tests/oidc/index.ts index 37490a0193089..2a49047d81a85 100644 --- a/x-pack/test/security_functional/tests/oidc/index.ts +++ b/x-pack/test/security_functional/tests/oidc/index.ts @@ -5,7 +5,7 @@ * 2.0. */ -import { FtrProviderContext } from '../../ftr_provider_context'; +import type { FtrProviderContext } from '../../ftr_provider_context'; export default function ({ loadTestFile }: FtrProviderContext) { describe('security app - OIDC interactions', function () { diff --git a/x-pack/test/security_functional/tests/oidc/url_capture.ts b/x-pack/test/security_functional/tests/oidc/url_capture.ts index 6553ef193fc3b..624e373c5eeca 100644 --- a/x-pack/test/security_functional/tests/oidc/url_capture.ts +++ b/x-pack/test/security_functional/tests/oidc/url_capture.ts @@ -5,9 +5,11 @@ * 2.0. */ -import expect from '@kbn/expect'; import { parse } from 'url'; -import { FtrProviderContext } from '../../ftr_provider_context'; + +import expect from '@kbn/expect'; + +import type { FtrProviderContext } from '../../ftr_provider_context'; export default function ({ getService, getPageObjects }: FtrProviderContext) { const find = getService('find'); diff --git a/x-pack/test/security_functional/tests/saml/index.ts b/x-pack/test/security_functional/tests/saml/index.ts index ebf97ebf8edfb..1cf0c4cedc7bb 100644 --- a/x-pack/test/security_functional/tests/saml/index.ts +++ b/x-pack/test/security_functional/tests/saml/index.ts @@ -5,7 +5,7 @@ * 2.0. */ -import { FtrProviderContext } from '../../ftr_provider_context'; +import type { FtrProviderContext } from '../../ftr_provider_context'; export default function ({ loadTestFile }: FtrProviderContext) { describe('security app - SAML interactions', function () { diff --git a/x-pack/test/security_functional/tests/saml/url_capture.ts b/x-pack/test/security_functional/tests/saml/url_capture.ts index 0193d3d870701..72bb342716cd0 100644 --- a/x-pack/test/security_functional/tests/saml/url_capture.ts +++ b/x-pack/test/security_functional/tests/saml/url_capture.ts @@ -5,9 +5,11 @@ * 2.0. */ -import expect from '@kbn/expect'; import { parse } from 'url'; -import { FtrProviderContext } from '../../ftr_provider_context'; + +import expect from '@kbn/expect'; + +import type { FtrProviderContext } from '../../ftr_provider_context'; export default function ({ getService, getPageObjects }: FtrProviderContext) { const find = getService('find'); diff --git a/x-pack/test/security_functional/tests/user_profiles/client_side_apis.ts b/x-pack/test/security_functional/tests/user_profiles/client_side_apis.ts index 0e1fb7879d81a..c6675eb526632 100644 --- a/x-pack/test/security_functional/tests/user_profiles/client_side_apis.ts +++ b/x-pack/test/security_functional/tests/user_profiles/client_side_apis.ts @@ -5,10 +5,12 @@ * 2.0. */ -import expect from '@kbn/expect'; import { parse as parseCookie } from 'tough-cookie'; + +import expect from '@kbn/expect'; import { adminTestUser } from '@kbn/test'; -import { FtrProviderContext } from '../../ftr_provider_context'; + +import type { FtrProviderContext } from '../../ftr_provider_context'; export default function ({ getService, getPageObjects }: FtrProviderContext) { const PageObjects = getPageObjects(['security', 'common']); diff --git a/x-pack/test/security_functional/tests/user_profiles/index.ts b/x-pack/test/security_functional/tests/user_profiles/index.ts index 85c74150dd3bd..4bb317365845f 100644 --- a/x-pack/test/security_functional/tests/user_profiles/index.ts +++ b/x-pack/test/security_functional/tests/user_profiles/index.ts @@ -5,7 +5,7 @@ * 2.0. */ -import { FtrProviderContext } from '../../ftr_provider_context'; +import type { FtrProviderContext } from '../../ftr_provider_context'; export default function ({ loadTestFile }: FtrProviderContext) { describe('security app - user profiles', function () { diff --git a/x-pack/test/security_functional/user_profiles.config.ts b/x-pack/test/security_functional/user_profiles.config.ts index e5b65db64c383..c52d29cd392d6 100644 --- a/x-pack/test/security_functional/user_profiles.config.ts +++ b/x-pack/test/security_functional/user_profiles.config.ts @@ -6,9 +6,11 @@ */ import { resolve } from 'path'; -import { FtrConfigProviderContext } from '@kbn/test'; -import { services } from '../functional/services'; + +import type { FtrConfigProviderContext } from '@kbn/test'; + import { pageObjects } from '../functional/page_objects'; +import { services } from '../functional/services'; // the default export of config files must be a config provider // that returns an object with the projects config values diff --git a/x-pack/test/security_solution_api_integration/config/ess/config.base.ts b/x-pack/test/security_solution_api_integration/config/ess/config.base.ts index 705c0b8686dd0..a0d2ee79a7b46 100644 --- a/x-pack/test/security_solution_api_integration/config/ess/config.base.ts +++ b/x-pack/test/security_solution_api_integration/config/ess/config.base.ts @@ -82,10 +82,8 @@ export function createTestConfig(options: CreateTestConfigOptions, testFiles?: s '--xpack.ruleRegistry.unsafe.legacyMultiTenancy.enabled=true', `--xpack.securitySolution.enableExperimental=${JSON.stringify([ 'previewTelemetryUrlEnabled', - 'loggingRequestsEnabled', 'riskScoringPersistence', 'riskScoringRoutesEnabled', - 'manualRuleRunEnabled', ])}`, '--xpack.task_manager.poll_interval=1000', `--xpack.actions.preconfigured=${JSON.stringify(PRECONFIGURED_ACTION_CONNECTORS)}`, diff --git a/x-pack/test/security_solution_api_integration/test_suites/detections_response/detection_engine/rule_execution_logic/trial_license_complete_tier/configs/serverless.config.ts b/x-pack/test/security_solution_api_integration/test_suites/detections_response/detection_engine/rule_execution_logic/trial_license_complete_tier/configs/serverless.config.ts index 8f64a859b7002..137ee1f67b9b3 100644 --- a/x-pack/test/security_solution_api_integration/test_suites/detections_response/detection_engine/rule_execution_logic/trial_license_complete_tier/configs/serverless.config.ts +++ b/x-pack/test/security_solution_api_integration/test_suites/detections_response/detection_engine/rule_execution_logic/trial_license_complete_tier/configs/serverless.config.ts @@ -17,9 +17,5 @@ export default createTestConfig({ 'testing_ignored.constant', '/testing_regex*/', ])}`, // See tests within the file "ignore_fields.ts" which use these values in "alertIgnoreFields" - `--xpack.securitySolution.enableExperimental=${JSON.stringify([ - 'manualRuleRunEnabled', - 'loggingRequestsEnabled', - ])}`, ], }); diff --git a/x-pack/test/security_solution_api_integration/test_suites/detections_response/detection_engine/rule_execution_logic/trial_license_complete_tier/execution_logic/eql.ts b/x-pack/test/security_solution_api_integration/test_suites/detections_response/detection_engine/rule_execution_logic/trial_license_complete_tier/execution_logic/eql.ts index aff2ccc6bccb3..9077873274fa5 100644 --- a/x-pack/test/security_solution_api_integration/test_suites/detections_response/detection_engine/rule_execution_logic/trial_license_complete_tier/execution_logic/eql.ts +++ b/x-pack/test/security_solution_api_integration/test_suites/detections_response/detection_engine/rule_execution_logic/trial_license_complete_tier/execution_logic/eql.ts @@ -1190,8 +1190,7 @@ export default ({ getService }: FtrProviderContext) => { }); }); - // skipped on MKI since feature flags are not supported there - describe('@skipInServerlessMKI preview logged requests', () => { + describe('preview logged requests', () => { it('should not return requests property when not enabled', async () => { const { logs } = await previewRule({ supertest, diff --git a/x-pack/test/security_solution_api_integration/test_suites/detections_response/detection_engine/rule_execution_logic/trial_license_complete_tier/execution_logic/esql.ts b/x-pack/test/security_solution_api_integration/test_suites/detections_response/detection_engine/rule_execution_logic/trial_license_complete_tier/execution_logic/esql.ts index 166a62b9b08ad..ee976de14186d 100644 --- a/x-pack/test/security_solution_api_integration/test_suites/detections_response/detection_engine/rule_execution_logic/trial_license_complete_tier/execution_logic/esql.ts +++ b/x-pack/test/security_solution_api_integration/test_suites/detections_response/detection_engine/rule_execution_logic/trial_license_complete_tier/execution_logic/esql.ts @@ -1409,8 +1409,7 @@ export default ({ getService }: FtrProviderContext) => { }); }); - // skipped on MKI since feature flags are not supported there - describe('@skipInServerlessMKI preview logged requests', () => { + describe('preview logged requests', () => { let rule: EsqlRuleCreateProps; let id: string; beforeEach(async () => { diff --git a/x-pack/test/security_solution_api_integration/test_suites/detections_response/detection_engine/rule_execution_logic/trial_license_complete_tier/execution_logic/index.ts b/x-pack/test/security_solution_api_integration/test_suites/detections_response/detection_engine/rule_execution_logic/trial_license_complete_tier/execution_logic/index.ts index 2dc37a8b900f7..ffb728e23d31b 100644 --- a/x-pack/test/security_solution_api_integration/test_suites/detections_response/detection_engine/rule_execution_logic/trial_license_complete_tier/execution_logic/index.ts +++ b/x-pack/test/security_solution_api_integration/test_suites/detections_response/detection_engine/rule_execution_logic/trial_license_complete_tier/execution_logic/index.ts @@ -23,6 +23,7 @@ export default ({ loadTestFile }: FtrProviderContext): void => { loadTestFile(require.resolve('./indicator_match_alert_suppression')); loadTestFile(require.resolve('./threshold')); loadTestFile(require.resolve('./threshold_alert_suppression')); + loadTestFile(require.resolve('./synthetic_source')); loadTestFile(require.resolve('./non_ecs_fields')); loadTestFile(require.resolve('./custom_query')); }); diff --git a/x-pack/test/security_solution_api_integration/test_suites/detections_response/detection_engine/rule_execution_logic/trial_license_complete_tier/execution_logic/synthetic_source.ts b/x-pack/test/security_solution_api_integration/test_suites/detections_response/detection_engine/rule_execution_logic/trial_license_complete_tier/execution_logic/synthetic_source.ts new file mode 100644 index 0000000000000..e70fa226213d5 --- /dev/null +++ b/x-pack/test/security_solution_api_integration/test_suites/detections_response/detection_engine/rule_execution_logic/trial_license_complete_tier/execution_logic/synthetic_source.ts @@ -0,0 +1,465 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license 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 'expect'; +import { v4 as uuidv4 } from 'uuid'; + +import { QueryRuleCreateProps } from '@kbn/security-solution-plugin/common/api/detection_engine'; +import { + getPreviewAlerts, + previewRule, + dataGeneratorFactory, + setSyntheticSource, +} from '../../../../utils'; +import { + deleteAllRules, + deleteAllAlerts, + getRuleForAlertTesting, +} from '../../../../../../../common/utils/security_solution'; +import { FtrProviderContext } from '../../../../../../ftr_provider_context'; + +export default ({ getService }: FtrProviderContext) => { + const supertest = getService('supertest'); + const esArchiver = getService('esArchiver'); + const es = getService('es'); + const log = getService('log'); + + const getRuleProps = (id: string, index: string): QueryRuleCreateProps => { + return { + ...getRuleForAlertTesting([index]), + query: `id:${id}`, + from: 'now-1h', + interval: '1h', + }; + }; + + describe('@ess @serverless synthetic source', () => { + describe('synthetic source limitations', () => { + const index = 'ecs_compliant'; + const { indexListOfDocuments } = dataGeneratorFactory({ es, index, log }); + + before(async () => { + await esArchiver.load(`x-pack/test/functional/es_archives/security_solution/${index}`); + await setSyntheticSource({ es, index }); + }); + + after(async () => { + await esArchiver.unload(`x-pack/test/functional/es_archives/security_solution/${index}`); + await deleteAllAlerts(supertest, log, es); + await deleteAllRules(supertest, log); + }); + + it('should convert dot-notation to nested objects', async () => { + const id = uuidv4(); + const timestamp = '2020-10-28T06:00:00.000Z'; + + const firstDoc = { + id, + '@timestamp': timestamp, + 'agent.name': 'agent-1', + }; + + await indexListOfDocuments([firstDoc]); + + const { previewId } = await previewRule({ + supertest, + rule: getRuleProps(id, index), + timeframeEnd: new Date('2020-10-28T06:30:00.000Z'), + invocationCount: 1, + }); + + const previewAlerts = await getPreviewAlerts({ + es, + previewId, + }); + + expect(previewAlerts.length).toEqual(1); + expect(previewAlerts[0]._source).toEqual({ + ...previewAlerts[0]._source, + // agent.name returned as nested object, but was indexed in original document with dot-notation + agent: { name: 'agent-1' }, + }); + }); + + it('should removed duplicated values in array', async () => { + const id = uuidv4(); + const timestamp = '2020-10-28T06:00:00.000Z'; + + const firstDoc = { + id, + '@timestamp': timestamp, + client: { ip: ['127.0.0.1', '127.0.0.1', '127.0.0.2'] }, + }; + + await indexListOfDocuments([firstDoc]); + + const { previewId } = await previewRule({ + supertest, + rule: getRuleProps(id, index), + timeframeEnd: new Date('2020-10-28T06:30:00.000Z'), + invocationCount: 1, + }); + + const previewAlerts = await getPreviewAlerts({ + es, + previewId, + }); + + expect(previewAlerts.length).toEqual(1); + expect(previewAlerts[0]._source).toEqual({ + ...previewAlerts[0]._source, + client: { ip: ['127.0.0.1', '127.0.0.2'] }, + }); + }); + + it('should sort duplicated values in array', async () => { + const id = uuidv4(); + const timestamp = '2020-10-28T06:00:00.000Z'; + + const firstDoc = { + id, + '@timestamp': timestamp, + client: { ip: ['127.0.0.3', '211.0.0.2', '127.0.0.1'] }, + }; + + await indexListOfDocuments([firstDoc]); + + const { previewId } = await previewRule({ + supertest, + rule: getRuleProps(id, index), + timeframeEnd: new Date('2020-10-28T06:30:00.000Z'), + invocationCount: 1, + }); + + const previewAlerts = await getPreviewAlerts({ + es, + previewId, + }); + + expect(previewAlerts.length).toEqual(1); + expect(previewAlerts[0]._source).toEqual({ + ...previewAlerts[0]._source, + client: { ip: ['127.0.0.1', '127.0.0.3', '211.0.0.2'] }, + }); + }); + + it('should convert array of objects to leaf structure', async () => { + const id = uuidv4(); + const timestamp = '2020-10-28T06:00:00.000Z'; + + const firstDoc = { + id, + '@timestamp': timestamp, + client: [{ ip: ['127.0.0.1'] }, { ip: ['127.0.0.2'] }], + }; + + await indexListOfDocuments([firstDoc]); + + const { previewId } = await previewRule({ + supertest, + rule: getRuleProps(id, index), + timeframeEnd: new Date('2020-10-28T06:30:00.000Z'), + invocationCount: 1, + }); + + const previewAlerts = await getPreviewAlerts({ + es, + previewId, + }); + + expect(previewAlerts.length).toEqual(1); + expect(previewAlerts[0]._source).toEqual({ + ...previewAlerts[0]._source, + client: { ip: ['127.0.0.1', '127.0.0.2'] }, + }); + }); + }); + + // this set of tests represent corrected failed test suits in https://github.com/elastic/kibana/pull/191527#issuecomment-2360684346 + // and ensures non-ecs fields are stripped when source mode is synthetic + describe('non ecs fields', () => { + const index = 'ecs_non_compliant'; + const { indexListOfDocuments } = dataGeneratorFactory({ es, index, log }); + const timestamp = '2020-10-28T06:00:00.000Z'; + + before(async () => { + await esArchiver.load(`x-pack/test/functional/es_archives/security_solution/${index}`); + await setSyntheticSource({ es, index }); + }); + + after(async () => { + await esArchiver.unload(`x-pack/test/functional/es_archives/security_solution/${index}`); + await deleteAllAlerts(supertest, log, es); + await deleteAllRules(supertest, log); + }); + + it('should not add multi field .text to ecs compliant flattened source', async () => { + const id = uuidv4(); + + const firstDoc = { + id, + '@timestamp': timestamp, + 'process.command_line': 'string longer than 10 characters', + }; + + await indexListOfDocuments([firstDoc]); + + const { previewId } = await previewRule({ + supertest, + rule: getRuleProps(id, index), + timeframeEnd: new Date('2020-10-28T06:30:00.000Z'), + invocationCount: 1, + }); + + const previewAlerts = await getPreviewAlerts({ + es, + previewId, + }); + + expect(previewAlerts[0]?._source?.process).toEqual({ + command_line: 'string longer than 10 characters', + }); + expect(previewAlerts[0]?._source).not.toHaveProperty('process.command_line.text'); + }); + + it('should not add multi field .text to ecs non compliant flattened source', async () => { + const id = uuidv4(); + + const firstDoc = { + id, + '@timestamp': timestamp, + 'nonEcs.command_line': 'string longer than 10 characters', + }; + + await indexListOfDocuments([firstDoc]); + + const { previewId } = await previewRule({ + supertest, + rule: getRuleProps(id, index), + timeframeEnd: new Date('2020-10-28T06:30:00.000Z'), + invocationCount: 1, + }); + + const previewAlerts = await getPreviewAlerts({ + es, + previewId, + }); + + expect(previewAlerts[0]?._source?.nonEcs).toEqual({ + command_line: 'string longer than 10 characters', + }); + expect(previewAlerts[0]?._source).not.toHaveProperty('process.nonEcs.text'); + }); + + it('should remove text field if the length of the string is more than 32766 bytes', async () => { + const id = uuidv4(); + + const document = { + id, + '@timestamp': timestamp, + 'event.original': 'z'.repeat(32767), + 'event.module': 'z'.repeat(32767), + 'event.action': 'z'.repeat(32767), + }; + + await indexListOfDocuments([document]); + + const { previewId } = await previewRule({ + supertest, + rule: getRuleProps(id, index), + timeframeEnd: new Date('2020-10-28T06:30:00.000Z'), + invocationCount: 1, + }); + + const previewAlerts = await getPreviewAlerts({ + es, + previewId, + }); + + const alertSource = previewAlerts[0]?._source; + + // keywords with `ignore_above` attribute which allows long text to be stored + expect(alertSource).toHaveProperty(['kibana.alert.original_event.module']); + expect(alertSource).toHaveProperty(['kibana.alert.original_event.original']); + expect(alertSource).toHaveProperty(['kibana.alert.original_event.action']); + + expect(alertSource?.event).toHaveProperty(['module']); + expect(alertSource?.event).toHaveProperty(['original']); + expect(alertSource?.event).toHaveProperty(['action']); + }); + + it('should not remove valid dates from ECS source field', async () => { + const id = uuidv4(); + + const validDates = [ + '2015-01-01T12:10:30.666Z', + '2015-01-01T12:10:30.666', + '2015-01-01T12:10:30Z', + '2015-01-01T12:10:30', + '2015-01-01T12:10Z', + '2015-01-01T12:10', + '2015-01-01T12Z', + '2015-01-01T12', + '2015-01-01', + '2015-01', + '2015-01-02T', + 123.3, + '23242', + -1, + '-1', + 0, + '0', + ]; + const document = { + id, + '@timestamp': timestamp, + event: { + created: validDates, + }, + }; + await indexListOfDocuments([document]); + + const { previewId } = await previewRule({ + supertest, + rule: getRuleProps(id, index), + timeframeEnd: new Date('2020-10-28T06:30:00.000Z'), + invocationCount: 1, + }); + + const previewAlerts = await getPreviewAlerts({ + es, + previewId, + }); + + // array of dates became sorted and duplicates removed + expect(previewAlerts[0]?._source).toHaveProperty( + ['event', 'created'], + [ + '-1', + '0', + '123.3', + '2015-01', + '2015-01-01', + '2015-01-01T12', + '2015-01-01T12:10', + '2015-01-01T12:10:30', + '2015-01-01T12:10:30.666', + '2015-01-01T12:10:30.666Z', + '2015-01-01T12:10:30Z', + '2015-01-01T12:10Z', + '2015-01-01T12Z', + '2015-01-02T', + '23242', + ] + ); + }); + + it('should not remove valid ips from ECS source field', async () => { + const id = uuidv4(); + const ip = [ + '127.0.0.1', + '::afff:4567:890a', + '::', + '::11.22.33.44', + '1111:2222:3333:4444:AAAA:BBBB:CCCC:DDDD', + ]; + + const document = { + id, + '@timestamp': timestamp, + client: { ip }, + }; + await indexListOfDocuments([document]); + + const { previewId } = await previewRule({ + supertest, + rule: getRuleProps(id, index), + timeframeEnd: new Date('2020-10-28T06:30:00.000Z'), + invocationCount: 1, + }); + + const previewAlerts = await getPreviewAlerts({ + es, + previewId, + }); + + // array of dates became sorted + expect(previewAlerts[0]?._source).toHaveProperty('client.ip', [ + '1111:2222:3333:4444:AAAA:BBBB:CCCC:DDDD', + '127.0.0.1', + '::', + '::11.22.33.44', + '::afff:4567:890a', + ]); + }); + + it('should remove source array of keywords field from alert if ECS field mapping is nested', async () => { + const id = uuidv4(); + + const document = { + id, + '@timestamp': timestamp, + threat: { + enrichments: ['non-valid-threat-1', 'non-valid-threat-2'], + 'indicator.port': 443, + }, + }; + await indexListOfDocuments([document]); + + const { previewId } = await previewRule({ + supertest, + rule: getRuleProps(id, index), + timeframeEnd: new Date('2020-10-28T06:30:00.000Z'), + invocationCount: 1, + }); + + const previewAlerts = await getPreviewAlerts({ + es, + previewId, + }); + + expect(previewAlerts[0]?._source).not.toHaveProperty('threat.enrichments'); + + expect(previewAlerts[0]?._source).toHaveProperty(['threat', 'indicator', 'port'], 443); + }); + + it('should strip invalid boolean values and left valid ones', async () => { + const id = uuidv4(); + + const document = { + id, + '@timestamp': timestamp, + dll: { + code_signature: { + valid: ['non-valid', 'true', 'false', [true, false], '', 'False', 'True', 1], + }, + }, + }; + await indexListOfDocuments([document]); + + const { previewId } = await previewRule({ + supertest, + rule: getRuleProps(id, index), + timeframeEnd: new Date('2020-10-28T06:30:00.000Z'), + invocationCount: 1, + }); + + const previewAlerts = await getPreviewAlerts({ + es, + previewId, + }); + + // invalid ECS values is getting removed, duplicates not stored in synthetic source + expect(previewAlerts[0]?._source).toHaveProperty('dll.code_signature.valid', [ + '', + 'false', + 'true', + ]); + }); + }); + }); +}; diff --git a/x-pack/test/security_solution_api_integration/test_suites/detections_response/detection_engine/rule_gaps/trial_license_complete_tier/configs/serverless.config.ts b/x-pack/test/security_solution_api_integration/test_suites/detections_response/detection_engine/rule_gaps/trial_license_complete_tier/configs/serverless.config.ts index 783adb64f6c2e..43904f7c217f3 100644 --- a/x-pack/test/security_solution_api_integration/test_suites/detections_response/detection_engine/rule_gaps/trial_license_complete_tier/configs/serverless.config.ts +++ b/x-pack/test/security_solution_api_integration/test_suites/detections_response/detection_engine/rule_gaps/trial_license_complete_tier/configs/serverless.config.ts @@ -16,6 +16,5 @@ export default createTestConfig({ 'testing_ignored.constant', '/testing_regex*/', ])}`, // See tests within the file "ignore_fields.ts" which use these values in "alertIgnoreFields" - `--xpack.securitySolution.enableExperimental=${JSON.stringify(['manualRuleRunEnabled'])}`, ], }); diff --git a/x-pack/test/security_solution_api_integration/test_suites/detections_response/detection_engine/rule_gaps/trial_license_complete_tier/manual_rule_run.ts b/x-pack/test/security_solution_api_integration/test_suites/detections_response/detection_engine/rule_gaps/trial_license_complete_tier/manual_rule_run.ts index 8a6167fc69301..153185456544d 100644 --- a/x-pack/test/security_solution_api_integration/test_suites/detections_response/detection_engine/rule_gaps/trial_license_complete_tier/manual_rule_run.ts +++ b/x-pack/test/security_solution_api_integration/test_suites/detections_response/detection_engine/rule_gaps/trial_license_complete_tier/manual_rule_run.ts @@ -42,9 +42,7 @@ export default ({ getService }: FtrProviderContext) => { const log = getService('log'); const es = getService('es'); - // Currently FF are not supported on MKI environments, so this test should be skipped from MKI environments. - // Once `manualRuleRunEnabled` FF is removed, we can remove `@skipInServerlessMKI` as well - describe('@ess @serverless @skipInServerlessMKI manual_rule_run', () => { + describe('@ess @serverless manual_rule_run', () => { beforeEach(async () => { await createAlertsIndex(supertest, log); }); diff --git a/x-pack/test/security_solution_api_integration/test_suites/detections_response/rules_management/rule_management/trial_license_complete_tier/configs/serverless.config.ts b/x-pack/test/security_solution_api_integration/test_suites/detections_response/rules_management/rule_management/trial_license_complete_tier/configs/serverless.config.ts index 52a1074c87904..ca9396db04661 100644 --- a/x-pack/test/security_solution_api_integration/test_suites/detections_response/rules_management/rule_management/trial_license_complete_tier/configs/serverless.config.ts +++ b/x-pack/test/security_solution_api_integration/test_suites/detections_response/rules_management/rule_management/trial_license_complete_tier/configs/serverless.config.ts @@ -12,7 +12,4 @@ export default createTestConfig({ reportName: 'Rules Management - Rule Management Integration Tests - Serverless Env - Complete Tier', }, - kbnTestServerArgs: [ - `--xpack.securitySolution.enableExperimental=${JSON.stringify(['manualRuleRunEnabled'])}`, - ], }); diff --git a/x-pack/test/security_solution_api_integration/test_suites/detections_response/rules_management/rule_patch/basic_license_essentials_tier/patch_rules.ts b/x-pack/test/security_solution_api_integration/test_suites/detections_response/rules_management/rule_patch/basic_license_essentials_tier/patch_rules.ts index a567eb78a776d..41f207c90f319 100644 --- a/x-pack/test/security_solution_api_integration/test_suites/detections_response/rules_management/rule_patch/basic_license_essentials_tier/patch_rules.ts +++ b/x-pack/test/security_solution_api_integration/test_suites/detections_response/rules_management/rule_patch/basic_license_essentials_tier/patch_rules.ts @@ -16,6 +16,9 @@ import { removeServerGeneratedPropertiesIncludingRuleId, getSimpleRuleOutputWithoutRuleId, updateUsername, + createHistoricalPrebuiltRuleAssetSavedObjects, + installPrebuiltRules, + createRuleAssetSavedObject, } from '../../../utils'; import { createAlertsIndex, @@ -238,6 +241,25 @@ export default ({ getService }: FtrProviderContext) => { }); }); + it('throws an error if rule has external rule source and non-customizable fields are changed', async () => { + // Install base prebuilt detection rule + await createHistoricalPrebuiltRuleAssetSavedObjects(es, [ + createRuleAssetSavedObject({ rule_id: 'rule-1', author: ['elastic'] }), + ]); + await installPrebuiltRules(es, supertest); + + const { body } = await securitySolutionApi + .patchRule({ + body: { + rule_id: 'rule-1', + author: ['new user'], + }, + }) + .expect(400); + + expect(body.message).toEqual('Cannot update "author" field for prebuilt rules'); + }); + describe('max signals', () => { afterEach(async () => { await deleteAllRules(supertest, log); diff --git a/x-pack/test/security_solution_api_integration/test_suites/detections_response/rules_management/rule_patch/basic_license_essentials_tier/patch_rules_bulk.ts b/x-pack/test/security_solution_api_integration/test_suites/detections_response/rules_management/rule_patch/basic_license_essentials_tier/patch_rules_bulk.ts index 086909fc4945b..7929b912768ff 100644 --- a/x-pack/test/security_solution_api_integration/test_suites/detections_response/rules_management/rule_patch/basic_license_essentials_tier/patch_rules_bulk.ts +++ b/x-pack/test/security_solution_api_integration/test_suites/detections_response/rules_management/rule_patch/basic_license_essentials_tier/patch_rules_bulk.ts @@ -16,6 +16,9 @@ import { getSimpleRuleOutputWithoutRuleId, removeServerGeneratedPropertiesIncludingRuleId, updateUsername, + createHistoricalPrebuiltRuleAssetSavedObjects, + installPrebuiltRules, + createRuleAssetSavedObject, } from '../../../utils'; import { createAlertsIndex, @@ -347,6 +350,41 @@ export default ({ getService }: FtrProviderContext) => { }, ]); }); + + it('throws an error if rule has external rule source and non-customizable fields are changed', async () => { + // Install base prebuilt detection rule + await createHistoricalPrebuiltRuleAssetSavedObjects(es, [ + createRuleAssetSavedObject({ rule_id: 'rule-1', author: ['elastic'] }), + createRuleAssetSavedObject({ rule_id: 'rule-2', license: 'basic' }), + ]); + await installPrebuiltRules(es, supertest); + + const { body } = await securitySolutionApi + .bulkPatchRules({ + body: [ + { rule_id: 'rule-1', author: ['new user'] }, + { rule_id: 'rule-2', license: 'new license' }, + ], + }) + .expect(200); + + expect([body[0], body[1]]).toEqual([ + { + error: { + message: 'Cannot update "author" field for prebuilt rules', + status_code: 400, + }, + rule_id: 'rule-1', + }, + { + error: { + message: 'Cannot update "license" field for prebuilt rules', + status_code: 400, + }, + rule_id: 'rule-2', + }, + ]); + }); }); }); }; diff --git a/x-pack/test/security_solution_api_integration/test_suites/detections_response/rules_management/rule_update/basic_license_essentials_tier/update_rules.ts b/x-pack/test/security_solution_api_integration/test_suites/detections_response/rules_management/rule_update/basic_license_essentials_tier/update_rules.ts index 60e7bfe3ff88f..c84236a14eb37 100644 --- a/x-pack/test/security_solution_api_integration/test_suites/detections_response/rules_management/rule_update/basic_license_essentials_tier/update_rules.ts +++ b/x-pack/test/security_solution_api_integration/test_suites/detections_response/rules_management/rule_update/basic_license_essentials_tier/update_rules.ts @@ -18,6 +18,9 @@ import { getSimpleMlRuleUpdate, getSimpleRule, updateUsername, + createHistoricalPrebuiltRuleAssetSavedObjects, + installPrebuiltRules, + createRuleAssetSavedObject, } from '../../../utils'; import { createAlertsIndex, @@ -309,6 +312,33 @@ export default ({ getService }: FtrProviderContext) => { expect(updatedRuleResponse).toMatchObject(expectedRule); }); }); + + it('throws an error if rule has external rule source and non-customizable fields are changed', async () => { + // Install base prebuilt detection rule + await createHistoricalPrebuiltRuleAssetSavedObjects(es, [ + createRuleAssetSavedObject({ rule_id: 'rule-1', license: 'elastic' }), + ]); + await installPrebuiltRules(es, supertest); + + const { body: existingRule } = await securitySolutionApi + .readRule({ + query: { rule_id: 'rule-1' }, + }) + .expect(200); + + const { body } = await securitySolutionApi + .updateRule({ + body: getCustomQueryRuleParams({ + ...existingRule, + rule_id: 'rule-1', + id: undefined, + license: 'new license', + }), + }) + .expect(400); + + expect(body.message).toEqual('Cannot update "license" field for prebuilt rules'); + }); }); }); }; diff --git a/x-pack/test/security_solution_api_integration/test_suites/detections_response/rules_management/rule_update/basic_license_essentials_tier/update_rules_bulk.ts b/x-pack/test/security_solution_api_integration/test_suites/detections_response/rules_management/rule_update/basic_license_essentials_tier/update_rules_bulk.ts index f9faee0481bf6..cdca9e3ca6e1a 100644 --- a/x-pack/test/security_solution_api_integration/test_suites/detections_response/rules_management/rule_update/basic_license_essentials_tier/update_rules_bulk.ts +++ b/x-pack/test/security_solution_api_integration/test_suites/detections_response/rules_management/rule_update/basic_license_essentials_tier/update_rules_bulk.ts @@ -17,6 +17,9 @@ import { getSimpleRuleUpdate, getSimpleRule, updateUsername, + createHistoricalPrebuiltRuleAssetSavedObjects, + installPrebuiltRules, + createRuleAssetSavedObject, } from '../../../utils'; import { createAlertsIndex, @@ -370,6 +373,30 @@ export default ({ getService }: FtrProviderContext) => { }, ]); }); + + it('throws an error if rule has external rule source and non-customizable fields are changed', async () => { + // Install base prebuilt detection rule + await createHistoricalPrebuiltRuleAssetSavedObjects(es, [ + createRuleAssetSavedObject({ rule_id: 'rule-1', author: ['elastic'] }), + ]); + await installPrebuiltRules(es, supertest); + + const { body } = await securitySolutionApi + .bulkUpdateRules({ + body: [getCustomQueryRuleParams({ rule_id: 'rule-1', author: ['new user'] })], + }) + .expect(200); + + expect([body[0]]).toEqual([ + { + error: { + message: 'Cannot update "author" field for prebuilt rules', + status_code: 400, + }, + rule_id: 'rule-1', + }, + ]); + }); }); }); }; diff --git a/x-pack/test/security_solution_api_integration/test_suites/detections_response/telemetry/trial_license_complete_tier/usage_collector/detection_rules.ts b/x-pack/test/security_solution_api_integration/test_suites/detections_response/telemetry/trial_license_complete_tier/usage_collector/detection_rules.ts index b3b58ac7880f8..c43d08a805ca8 100644 --- a/x-pack/test/security_solution_api_integration/test_suites/detections_response/telemetry/trial_license_complete_tier/usage_collector/detection_rules.ts +++ b/x-pack/test/security_solution_api_integration/test_suites/detections_response/telemetry/trial_license_complete_tier/usage_collector/detection_rules.ts @@ -31,6 +31,7 @@ import { getRuleSavedObjectWithLegacyInvestigationFields, getRuleSavedObjectWithLegacyInvestigationFieldsEmptyArray, createRuleThroughAlertingEndpoint, + getCustomQueryRuleParams, } from '../../../utils'; import { createRule, @@ -1140,7 +1141,7 @@ export default ({ getService }: FtrProviderContext) => { await installMockPrebuiltRules(supertest, es); const immutableRule = await fetchRule(supertest, { ruleId: ELASTIC_SECURITY_RULE_ID }); const hookAction = await createWebHookRuleAction(supertest); - const newRuleToUpdate = getSimpleRule(immutableRule.rule_id); + const newRuleToUpdate = getCustomQueryRuleParams({ rule_id: immutableRule.rule_id }); const ruleToUpdate = getRuleWithWebHookAction(hookAction.id, false, newRuleToUpdate); await updateRule(supertest, ruleToUpdate); @@ -1161,7 +1162,7 @@ export default ({ getService }: FtrProviderContext) => { ...omittedFields } = foundRule; expect(omittedFields).to.eql({ - rule_name: 'Simple Rule Query', + rule_name: 'Custom query rule', rule_type: 'query', enabled: false, elastic_rule: true, @@ -1197,7 +1198,7 @@ export default ({ getService }: FtrProviderContext) => { await installMockPrebuiltRules(supertest, es); const immutableRule = await fetchRule(supertest, { ruleId: ELASTIC_SECURITY_RULE_ID }); const hookAction = await createWebHookRuleAction(supertest); - const newRuleToUpdate = getSimpleRule(immutableRule.rule_id); + const newRuleToUpdate = getCustomQueryRuleParams({ rule_id: immutableRule.rule_id }); const ruleToUpdate = getRuleWithWebHookAction(hookAction.id, true, newRuleToUpdate); await updateRule(supertest, ruleToUpdate); @@ -1218,7 +1219,7 @@ export default ({ getService }: FtrProviderContext) => { ...omittedFields } = foundRule; expect(omittedFields).to.eql({ - rule_name: 'Simple Rule Query', + rule_name: 'Custom query rule', rule_type: 'query', enabled: true, elastic_rule: true, @@ -1254,7 +1255,7 @@ export default ({ getService }: FtrProviderContext) => { await installMockPrebuiltRules(supertest, es); const immutableRule = await fetchRule(supertest, { ruleId: ELASTIC_SECURITY_RULE_ID }); const hookAction = await createWebHookRuleAction(supertest); - const newRuleToUpdate = getSimpleRule(immutableRule.rule_id, false); + const newRuleToUpdate = getCustomQueryRuleParams({ rule_id: immutableRule.rule_id }); await updateRule(supertest, newRuleToUpdate); await createLegacyRuleAction(supertest, immutableRule.id, hookAction.id); @@ -1275,7 +1276,7 @@ export default ({ getService }: FtrProviderContext) => { ...omittedFields } = foundRule; expect(omittedFields).to.eql({ - rule_name: 'Simple Rule Query', + rule_name: 'Custom query rule', rule_type: 'query', enabled: false, elastic_rule: true, @@ -1311,7 +1312,10 @@ export default ({ getService }: FtrProviderContext) => { await installMockPrebuiltRules(supertest, es); const immutableRule = await fetchRule(supertest, { ruleId: ELASTIC_SECURITY_RULE_ID }); const hookAction = await createWebHookRuleAction(supertest); - const newRuleToUpdate = getSimpleRule(immutableRule.rule_id, true); + const newRuleToUpdate = getCustomQueryRuleParams({ + rule_id: immutableRule.rule_id, + enabled: true, + }); await updateRule(supertest, newRuleToUpdate); await createLegacyRuleAction(supertest, immutableRule.id, hookAction.id); @@ -1332,7 +1336,7 @@ export default ({ getService }: FtrProviderContext) => { ...omittedFields } = foundRule; expect(omittedFields).to.eql({ - rule_name: 'Simple Rule Query', + rule_name: 'Custom query rule', rule_type: 'query', enabled: true, elastic_rule: true, diff --git a/x-pack/test/security_solution_api_integration/test_suites/detections_response/telemetry/trial_license_complete_tier/usage_collector/detection_rules_legacy_action.ts b/x-pack/test/security_solution_api_integration/test_suites/detections_response/telemetry/trial_license_complete_tier/usage_collector/detection_rules_legacy_action.ts index e3754d9a09b60..f85f317e2da07 100644 --- a/x-pack/test/security_solution_api_integration/test_suites/detections_response/telemetry/trial_license_complete_tier/usage_collector/detection_rules_legacy_action.ts +++ b/x-pack/test/security_solution_api_integration/test_suites/detections_response/telemetry/trial_license_complete_tier/usage_collector/detection_rules_legacy_action.ts @@ -21,13 +21,13 @@ import { fetchRule, getRuleWithWebHookAction, getSimpleMlRule, - getSimpleRule, getSimpleThreatMatch, getStats, getThresholdRuleForAlertTesting, installMockPrebuiltRules, updateRule, deleteAllEventLogExecutionEvents, + getCustomQueryRuleParams, } from '../../../utils'; import { createRule, @@ -408,7 +408,7 @@ export default ({ getService }: FtrProviderContext) => { await installMockPrebuiltRules(supertest, es); const immutableRule = await fetchRule(supertest, { ruleId: ELASTIC_SECURITY_RULE_ID }); const hookAction = await createWebHookRuleAction(supertest); - const newRuleToUpdate = getSimpleRule(immutableRule.rule_id, false); + const newRuleToUpdate = getCustomQueryRuleParams({ rule_id: immutableRule.rule_id }); await updateRule(supertest, newRuleToUpdate); await createLegacyRuleAction(supertest, immutableRule.id, hookAction.id); @@ -429,7 +429,7 @@ export default ({ getService }: FtrProviderContext) => { ...omittedFields } = foundRule; expect(omittedFields).to.eql({ - rule_name: 'Simple Rule Query', + rule_name: 'Custom query rule', rule_type: 'query', enabled: false, elastic_rule: true, @@ -465,7 +465,10 @@ export default ({ getService }: FtrProviderContext) => { await installMockPrebuiltRules(supertest, es); const immutableRule = await fetchRule(supertest, { ruleId: ELASTIC_SECURITY_RULE_ID }); const hookAction = await createWebHookRuleAction(supertest); - const newRuleToUpdate = getSimpleRule(immutableRule.rule_id, true); + const newRuleToUpdate = getCustomQueryRuleParams({ + rule_id: immutableRule.rule_id, + enabled: true, + }); await updateRule(supertest, newRuleToUpdate); await createLegacyRuleAction(supertest, immutableRule.id, hookAction.id); @@ -486,7 +489,7 @@ export default ({ getService }: FtrProviderContext) => { ...omittedFields } = foundRule; expect(omittedFields).to.eql({ - rule_name: 'Simple Rule Query', + rule_name: 'Custom query rule', rule_type: 'query', enabled: true, elastic_rule: true, diff --git a/x-pack/test/security_solution_api_integration/test_suites/detections_response/utils/index.ts b/x-pack/test/security_solution_api_integration/test_suites/detections_response/utils/index.ts index 2ce85256b0fbf..5667762ce95c4 100644 --- a/x-pack/test/security_solution_api_integration/test_suites/detections_response/utils/index.ts +++ b/x-pack/test/security_solution_api_integration/test_suites/detections_response/utils/index.ts @@ -12,6 +12,7 @@ export * from './data_generator'; export * from './telemetry'; export * from './event_log'; export * from './machine_learning'; +export * from './indices'; export * from './binary_to_string'; export * from './get_index_name_from_load'; diff --git a/x-pack/test/security_solution_api_integration/test_suites/detections_response/utils/indices/index.ts b/x-pack/test/security_solution_api_integration/test_suites/detections_response/utils/indices/index.ts new file mode 100644 index 0000000000000..79cad822b8f36 --- /dev/null +++ b/x-pack/test/security_solution_api_integration/test_suites/detections_response/utils/indices/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 './set_synthetic_source'; diff --git a/x-pack/test/security_solution_api_integration/test_suites/detections_response/utils/indices/set_synthetic_source.ts b/x-pack/test/security_solution_api_integration/test_suites/detections_response/utils/indices/set_synthetic_source.ts new file mode 100644 index 0000000000000..b37bcd7664319 --- /dev/null +++ b/x-pack/test/security_solution_api_integration/test_suites/detections_response/utils/indices/set_synthetic_source.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 type { Client } from '@elastic/elasticsearch'; + +interface UpdateMappingsProps { + es: Client; + index: string | string[]; +} + +export const setSyntheticSource = async ({ es, index }: UpdateMappingsProps) => { + await es.indices.putMapping({ _source: { mode: 'synthetic' }, index }); +}; diff --git a/x-pack/test/security_solution_api_integration/test_suites/detections_response/utils/rules/get_rule_params/get_custom_query_rule_params.ts b/x-pack/test/security_solution_api_integration/test_suites/detections_response/utils/rules/get_rule_params/get_custom_query_rule_params.ts index b561d3e8dc023..a5c5fe00ed700 100644 --- a/x-pack/test/security_solution_api_integration/test_suites/detections_response/utils/rules/get_rule_params/get_custom_query_rule_params.ts +++ b/x-pack/test/security_solution_api_integration/test_suites/detections_response/utils/rules/get_rule_params/get_custom_query_rule_params.ts @@ -29,6 +29,7 @@ export function getCustomQueryRuleParams( index: ['logs-*'], interval: '100m', from: 'now-6m', + author: [], enabled: false, ...rewrites, }; diff --git a/x-pack/test/security_solution_api_integration/test_suites/edr_workflows/authentication/trial_license_complete_tier/endpoint_authz.ts b/x-pack/test/security_solution_api_integration/test_suites/edr_workflows/authentication/trial_license_complete_tier/endpoint_authz.ts index 5805dbef73e2e..c8dd877849b35 100644 --- a/x-pack/test/security_solution_api_integration/test_suites/edr_workflows/authentication/trial_license_complete_tier/endpoint_authz.ts +++ b/x-pack/test/security_solution_api_integration/test_suites/edr_workflows/authentication/trial_license_complete_tier/endpoint_authz.ts @@ -19,7 +19,7 @@ import { HOST_METADATA_LIST_ROUTE, ISOLATE_HOST_ROUTE_V2, KILL_PROCESS_ROUTE, - METADATA_TRANSFORMS_STATUS_ROUTE, + METADATA_TRANSFORMS_STATUS_INTERNAL_ROUTE, SUSPEND_PROCESS_ROUTE, UNISOLATE_HOST_ROUTE_V2, } from '@kbn/security-solution-plugin/common/endpoint/constants'; @@ -31,6 +31,7 @@ import { ROLE } from '../../../../config/services/security_solution_edr_workflow export default function ({ getService }: FtrProviderContext) { const endpointTestResources = getService('endpointTestResources'); const utils = getService('securitySolutionUtils'); + const samlAuth = getService('samlAuth'); interface ApiCallsInterface { method: keyof Pick; @@ -70,7 +71,8 @@ export default function ({ getService }: FtrProviderContext) { }, { method: 'get', - path: METADATA_TRANSFORMS_STATUS_ROUTE, + path: METADATA_TRANSFORMS_STATUS_INTERNAL_ROUTE, + version: '1', body: undefined, }, { @@ -210,6 +212,11 @@ export default function ({ getService }: FtrProviderContext) { }]`, async () => { await t1AnalystSupertest[apiListItem.method](replacePathIds(apiListItem.path)) .set('kbn-xsrf', 'xxx') + .set( + apiListItem.version ? 'Elastic-Api-Version' : 'foo', + apiListItem.version || '2023-10-31' + ) + .set(samlAuth.getInternalRequestHeader()) .send(getBodyPayload(apiListItem)) .expect(200); }); @@ -246,6 +253,11 @@ export default function ({ getService }: FtrProviderContext) { }]`, async () => { await platformEnginnerSupertest[apiListItem.method](replacePathIds(apiListItem.path)) .set('kbn-xsrf', 'xxx') + .set( + apiListItem.version ? 'Elastic-Api-Version' : 'foo', + apiListItem.version || '2023-10-31' + ) + .set(samlAuth.getInternalRequestHeader()) .send(getBodyPayload(apiListItem)) .expect(200); }); @@ -283,6 +295,11 @@ export default function ({ getService }: FtrProviderContext) { await endpointOperationsAnalystSupertest[apiListItem.method]( replacePathIds(apiListItem.path) ) + .set( + apiListItem.version ? 'Elastic-Api-Version' : 'foo', + apiListItem.version || '2023-10-31' + ) + .set(samlAuth.getInternalRequestHeader()) .set('kbn-xsrf', 'xxx') .send(getBodyPayload(apiListItem)) .expect(200); @@ -304,6 +321,11 @@ export default function ({ getService }: FtrProviderContext) { }]`, async () => { await adminSupertest[apiListItem.method](replacePathIds(apiListItem.path)) .set('kbn-xsrf', 'xxx') + .set( + apiListItem.version ? 'Elastic-Api-Version' : 'foo', + apiListItem.version || '2023-10-31' + ) + .set(samlAuth.getInternalRequestHeader()) .send(getBodyPayload(apiListItem)) .expect(200); }); diff --git a/x-pack/test/security_solution_api_integration/test_suites/edr_workflows/metadata/trial_license_complete_tier/metadata.ts b/x-pack/test/security_solution_api_integration/test_suites/edr_workflows/metadata/trial_license_complete_tier/metadata.ts index 8dac3bfe66784..ee4ee09bbdad6 100644 --- a/x-pack/test/security_solution_api_integration/test_suites/edr_workflows/metadata/trial_license_complete_tier/metadata.ts +++ b/x-pack/test/security_solution_api_integration/test_suites/edr_workflows/metadata/trial_license_complete_tier/metadata.ts @@ -13,12 +13,12 @@ import { ENDPOINT_DEFAULT_SORT_FIELD, HOST_METADATA_LIST_ROUTE, METADATA_DATASTREAM, - METADATA_TRANSFORMS_STATUS_ROUTE, METADATA_UNITED_INDEX, METADATA_UNITED_TRANSFORM, METADATA_UNITED_TRANSFORM_V2, metadataTransformPrefix, METADATA_CURRENT_TRANSFORM_V2, + METADATA_TRANSFORMS_STATUS_INTERNAL_ROUTE, } from '@kbn/security-solution-plugin/common/endpoint/constants'; import { AGENTS_INDEX } from '@kbn/fleet-plugin/common'; import { indexFleetEndpointPolicy } from '@kbn/security-solution-plugin/common/endpoint/data_loaders/index_fleet_endpoint_policy'; @@ -426,9 +426,9 @@ export default function ({ getService }: FtrProviderContext) { const ca = config.get('servers.kibana').certificateAuthorities; await t1AnalystSupertest - .get(METADATA_TRANSFORMS_STATUS_ROUTE) + .get(METADATA_TRANSFORMS_STATUS_INTERNAL_ROUTE) .set('kbn-xsrf', 'xxx') - .set('Elastic-Api-Version', '2023-10-31') + .set('Elastic-Api-Version', '1') .ca(ca) .expect(401); }); @@ -438,9 +438,9 @@ export default function ({ getService }: FtrProviderContext) { await endpointDataStreamHelpers.stopTransform(getService, `${unitedTransformName}*`); const { body } = await adminSupertest - .get(METADATA_TRANSFORMS_STATUS_ROUTE) + .get(METADATA_TRANSFORMS_STATUS_INTERNAL_ROUTE) .set('kbn-xsrf', 'xxx') - .set('Elastic-Api-Version', '2023-10-31') + .set('Elastic-Api-Version', '1') .expect(200); const transforms = (body.transforms as TransformGetTransformStatsTransformStats[]).filter( @@ -466,9 +466,9 @@ export default function ({ getService }: FtrProviderContext) { it('correctly returns started transform stats', async () => { const { body } = await adminSupertest - .get(METADATA_TRANSFORMS_STATUS_ROUTE) + .get(METADATA_TRANSFORMS_STATUS_INTERNAL_ROUTE) .set('kbn-xsrf', 'xxx') - .set('Elastic-Api-Version', '2023-10-31') + .set('Elastic-Api-Version', '1') .expect(200); const transforms = (body.transforms as TransformGetTransformStatsTransformStats[]).filter( diff --git a/x-pack/test/security_solution_api_integration/test_suites/entity_analytics/entity_store/trial_license_complete_tier/engine.ts b/x-pack/test/security_solution_api_integration/test_suites/entity_analytics/entity_store/trial_license_complete_tier/engine.ts index 99d84fbc5427b..4fb2360a049cf 100644 --- a/x-pack/test/security_solution_api_integration/test_suites/entity_analytics/entity_store/trial_license_complete_tier/engine.ts +++ b/x-pack/test/security_solution_api_integration/test_suites/entity_analytics/entity_store/trial_license_complete_tier/engine.ts @@ -27,10 +27,7 @@ export default ({ getService }: FtrProviderContext) => { it('should have installed the expected user resources', async () => { await utils.initEntityEngineForEntityType('user'); - const expectedTransforms = [ - 'entities-v1-history-ea_default_user_entity_store', - 'entities-v1-latest-ea_default_user_entity_store', - ]; + const expectedTransforms = ['entities-v1-latest-ea_default_user_entity_store']; await utils.expectTransformsExist(expectedTransforms); }); @@ -38,10 +35,7 @@ export default ({ getService }: FtrProviderContext) => { it('should have installed the expected host resources', async () => { await utils.initEntityEngineForEntityType('host'); - const expectedTransforms = [ - 'entities-v1-history-ea_default_host_entity_store', - 'entities-v1-latest-ea_default_host_entity_store', - ]; + const expectedTransforms = ['entities-v1-latest-ea_default_host_entity_store']; await utils.expectTransformsExist(expectedTransforms); }); @@ -173,7 +167,6 @@ export default ({ getService }: FtrProviderContext) => { }) .expect(200); - await utils.expectTransformNotFound('entities-v1-history-ea_host_entity_store'); await utils.expectTransformNotFound('entities-v1-latest-ea_host_entity_store'); }); @@ -187,7 +180,6 @@ export default ({ getService }: FtrProviderContext) => { }) .expect(200); - await utils.expectTransformNotFound('entities-v1-history-ea_user_entity_store'); await utils.expectTransformNotFound('entities-v1-latest-ea_user_entity_store'); }); }); diff --git a/x-pack/test/security_solution_api_integration/test_suites/entity_analytics/entity_store/trial_license_complete_tier/engine_nondefault_spaces.ts b/x-pack/test/security_solution_api_integration/test_suites/entity_analytics/entity_store/trial_license_complete_tier/engine_nondefault_spaces.ts index 112c8b8b21511..e3ef29d937183 100644 --- a/x-pack/test/security_solution_api_integration/test_suites/entity_analytics/entity_store/trial_license_complete_tier/engine_nondefault_spaces.ts +++ b/x-pack/test/security_solution_api_integration/test_suites/entity_analytics/entity_store/trial_license_complete_tier/engine_nondefault_spaces.ts @@ -38,10 +38,7 @@ export default ({ getService }: FtrProviderContextWithSpaces) => { it('should have installed the expected user resources', async () => { await utils.initEntityEngineForEntityType('user'); - const expectedTransforms = [ - `entities-v1-history-ea_${namespace}_user_entity_store`, - `entities-v1-latest-ea_${namespace}_user_entity_store`, - ]; + const expectedTransforms = [`entities-v1-latest-ea_${namespace}_user_entity_store`]; await utils.expectTransformsExist(expectedTransforms); }); @@ -49,10 +46,7 @@ export default ({ getService }: FtrProviderContextWithSpaces) => { it('should have installed the expected host resources', async () => { await utils.initEntityEngineForEntityType('host'); - const expectedTransforms = [ - `entities-v1-history-ea_${namespace}_host_entity_store`, - `entities-v1-latest-ea_${namespace}_host_entity_store`, - ]; + const expectedTransforms = [`entities-v1-latest-ea_${namespace}_host_entity_store`]; await utils.expectTransformsExist(expectedTransforms); }); diff --git a/x-pack/test/security_solution_api_integration/test_suites/entity_analytics/risk_engine/trial_license_complete_tier/init_and_status_apis.ts b/x-pack/test/security_solution_api_integration/test_suites/entity_analytics/risk_engine/trial_license_complete_tier/init_and_status_apis.ts index bd3493b82d348..19a9bb85326fa 100644 --- a/x-pack/test/security_solution_api_integration/test_suites/entity_analytics/risk_engine/trial_license_complete_tier/init_and_status_apis.ts +++ b/x-pack/test/security_solution_api_integration/test_suites/entity_analytics/risk_engine/trial_license_complete_tier/init_and_status_apis.ts @@ -26,8 +26,7 @@ export default ({ getService }: FtrProviderContext) => { const riskEngineRoutes = riskEngineRouteHelpersFactory(supertest); const log = getService('log'); - // Failing: See https://github.com/elastic/kibana/issues/191637 - describe.skip('@ess @serverless @serverlessQA init_and_status_apis', () => { + describe('@ess @serverless @serverlessQA init_and_status_apis', () => { before(async () => { await riskEngineRoutes.cleanUp(); }); @@ -298,8 +297,8 @@ export default ({ getService }: FtrProviderContext) => { firstResponse?.saved_objects?.[0]?.id ); }); - - describe('remove legacy risk score transform', function () { + // Failing: See https://github.com/elastic/kibana/issues/191637 + describe.skip('remove legacy risk score transform', function () { this.tags('skipFIPS'); it('should remove legacy risk score transform if it exists', async () => { await installLegacyRiskScore({ supertest }); diff --git a/x-pack/test/security_solution_cypress/config.ts b/x-pack/test/security_solution_cypress/config.ts index 88752eb1b5f93..f02968945087d 100644 --- a/x-pack/test/security_solution_cypress/config.ts +++ b/x-pack/test/security_solution_cypress/config.ts @@ -44,10 +44,6 @@ export default async function ({ readConfigFile }: FtrConfigProviderContext) { // See https://github.com/elastic/kibana/pull/125396 for details '--xpack.alerting.rules.minimumScheduleInterval.value=1s', '--xpack.ruleRegistry.unsafe.legacyMultiTenancy.enabled=true', - `--xpack.securitySolution.enableExperimental=${JSON.stringify([ - 'manualRuleRunEnabled', - 'loggingRequestsEnabled', - ])}`, // mock cloud to enable the guided onboarding tour in e2e tests '--xpack.cloud.id=test', `--home.disableWelcomeScreen=true`, diff --git a/x-pack/test/security_solution_cypress/cypress/e2e/detection_response/detection_engine/rule_creation/esql_rule.cy.ts b/x-pack/test/security_solution_cypress/cypress/e2e/detection_response/detection_engine/rule_creation/esql_rule.cy.ts index 0045a79ff4394..64423a921e595 100644 --- a/x-pack/test/security_solution_cypress/cypress/e2e/detection_response/detection_engine/rule_creation/esql_rule.cy.ts +++ b/x-pack/test/security_solution_cypress/cypress/e2e/detection_response/detection_engine/rule_creation/esql_rule.cy.ts @@ -67,7 +67,8 @@ const workaroundForResizeObserver = () => } }); -describe( +// Failing: See https://github.com/elastic/kibana/issues/184558 +describe.skip( 'Detection ES|QL rules, creation', { tags: ['@ess', '@serverless', '@skipInServerlessMKI'], diff --git a/x-pack/test/security_solution_cypress/cypress/e2e/detection_response/detection_engine/rule_creation/indicator_match_rule_suppression.cy.ts b/x-pack/test/security_solution_cypress/cypress/e2e/detection_response/detection_engine/rule_creation/indicator_match_rule_suppression.cy.ts index 42fb37184da1c..d0539683e5a64 100644 --- a/x-pack/test/security_solution_cypress/cypress/e2e/detection_response/detection_engine/rule_creation/indicator_match_rule_suppression.cy.ts +++ b/x-pack/test/security_solution_cypress/cypress/e2e/detection_response/detection_engine/rule_creation/indicator_match_rule_suppression.cy.ts @@ -12,7 +12,6 @@ import { SUPPRESS_FOR_DETAILS, SUPPRESS_BY_DETAILS, SUPPRESS_MISSING_FIELD, - DETAILS_TITLE, } from '../../../../screens/rule_details'; import { @@ -67,9 +66,6 @@ describe( 'have.text', 'Suppress and group alerts for events with missing fields' ); - - // suppression functionality should be under Tech Preview - cy.contains(DETAILS_TITLE, SUPPRESS_FOR_DETAILS).contains('Technical Preview'); }); fillAboutRuleMinimumAndContinue(rule); diff --git a/x-pack/test/security_solution_cypress/cypress/e2e/detection_response/detection_engine/rule_creation/indicator_match_rule_suppression_ess_basic.cy.ts b/x-pack/test/security_solution_cypress/cypress/e2e/detection_response/detection_engine/rule_creation/indicator_match_rule_suppression_ess_basic.cy.ts index dd3c086224e49..6223ac017281d 100644 --- a/x-pack/test/security_solution_cypress/cypress/e2e/detection_response/detection_engine/rule_creation/indicator_match_rule_suppression_ess_basic.cy.ts +++ b/x-pack/test/security_solution_cypress/cypress/e2e/detection_response/detection_engine/rule_creation/indicator_match_rule_suppression_ess_basic.cy.ts @@ -9,7 +9,6 @@ import { getNewThreatIndicatorRule } from '../../../../objects/rule'; import { SUPPRESS_FOR_DETAILS, - DETAILS_TITLE, SUPPRESS_BY_DETAILS, SUPPRESS_MISSING_FIELD, DEFINITION_DETAILS, @@ -62,9 +61,6 @@ describe( 'have.text', 'Do not suppress alerts for events with missing fields' ); - - // suppression functionality should be under Tech Preview - cy.contains(DETAILS_TITLE, SUPPRESS_FOR_DETAILS).contains('Technical Preview'); }); // Platinum license is required for configuration to apply diff --git a/x-pack/test/security_solution_cypress/cypress/e2e/detection_response/detection_engine/rule_creation/machine_learning_rule_suppression.cy.ts b/x-pack/test/security_solution_cypress/cypress/e2e/detection_response/detection_engine/rule_creation/machine_learning_rule_suppression.cy.ts index c38a6ef43150a..45ccc2c5aba8d 100644 --- a/x-pack/test/security_solution_cypress/cypress/e2e/detection_response/detection_engine/rule_creation/machine_learning_rule_suppression.cy.ts +++ b/x-pack/test/security_solution_cypress/cypress/e2e/detection_response/detection_engine/rule_creation/machine_learning_rule_suppression.cy.ts @@ -13,7 +13,6 @@ import { } from '../../../../screens/create_new_rule'; import { DEFINITION_DETAILS, - DETAILS_TITLE, SUPPRESS_BY_DETAILS, SUPPRESS_FOR_DETAILS, SUPPRESS_MISSING_FIELD, @@ -129,9 +128,6 @@ describe( 'have.text', 'Suppress and group alerts for events with missing fields' ); - - // suppression functionality should be under Tech Preview - cy.contains(DETAILS_TITLE, SUPPRESS_FOR_DETAILS).contains('Technical Preview'); }); fillAboutRuleMinimumAndContinue(mlRule); @@ -163,9 +159,6 @@ describe( 'have.text', 'Do not suppress alerts for events with missing fields' ); - - // suppression functionality should be under Tech Preview - cy.contains(DETAILS_TITLE, SUPPRESS_FOR_DETAILS).contains('Technical Preview'); }); fillAboutRuleMinimumAndContinue(mlRule); diff --git a/x-pack/test/security_solution_cypress/cypress/e2e/detection_response/detection_engine/rule_creation/threshold_rule.cy.ts b/x-pack/test/security_solution_cypress/cypress/e2e/detection_response/detection_engine/rule_creation/threshold_rule.cy.ts index 11740c1f795f8..8af755c6ed328 100644 --- a/x-pack/test/security_solution_cypress/cypress/e2e/detection_response/detection_engine/rule_creation/threshold_rule.cy.ts +++ b/x-pack/test/security_solution_cypress/cypress/e2e/detection_response/detection_engine/rule_creation/threshold_rule.cy.ts @@ -58,6 +58,8 @@ import { fillScheduleRuleAndContinue, selectThresholdRuleType, waitForAlertsToPopulate, + fillDefineThresholdRule, + continueFromDefineStep, } from '../../../../tasks/create_new_rule'; import { login } from '../../../../tasks/login'; import { visit } from '../../../../tasks/navigation'; @@ -68,7 +70,7 @@ import { CREATE_RULE_URL } from '../../../../urls/navigation'; describe( 'Threshold rules', { - tags: ['@ess', '@serverless', '@skipInServerlessMKI'], + tags: ['@ess', '@serverless'], }, () => { const rule = getNewThresholdRule(); @@ -152,8 +154,10 @@ describe( it('Creates a new threshold rule with suppression enabled', () => { selectThresholdRuleType(); + fillDefineThresholdRule(rule); enablesAndPopulatesThresholdSuppression(5, 'h'); - fillDefineThresholdRuleAndContinue(rule); + continueFromDefineStep(); + // ensures duration displayed on define step in preview mode cy.get(DEFINITION_DETAILS).within(() => { getDetails(SUPPRESS_FOR_DETAILS).should('have.text', '5h'); diff --git a/x-pack/test/security_solution_cypress/cypress/e2e/detection_response/detection_engine/rule_edit/esql_rule.cy.ts b/x-pack/test/security_solution_cypress/cypress/e2e/detection_response/detection_engine/rule_edit/esql_rule.cy.ts index 511ea42c06767..34f301602b692 100644 --- a/x-pack/test/security_solution_cypress/cypress/e2e/detection_response/detection_engine/rule_edit/esql_rule.cy.ts +++ b/x-pack/test/security_solution_cypress/cypress/e2e/detection_response/detection_engine/rule_edit/esql_rule.cy.ts @@ -14,7 +14,6 @@ import { DEFINITION_DETAILS, SUPPRESS_MISSING_FIELD, SUPPRESS_BY_DETAILS, - DETAILS_TITLE, } from '../../../../screens/rule_details'; import { @@ -56,7 +55,9 @@ const expectedValidEsqlQuery = 'from auditbeat* | stats _count=count(event.category) by event.category'; // Skipping in MKI due to flake -describe( +// Failing: See https://github.com/elastic/kibana/issues/184557 +// Failing: See https://github.com/elastic/kibana/issues/184556 +describe.skip( 'Detection ES|QL rules, edit', { tags: ['@ess', '@serverless', '@skipInServerlessMKI'], @@ -191,9 +192,6 @@ describe( 'have.text', 'Suppress and group alerts for events with missing fields' ); - - // suppression functionality should be under Tech Preview - cy.contains(DETAILS_TITLE, SUPPRESS_FOR_DETAILS).contains('Technical Preview'); }); }); }); diff --git a/x-pack/test/security_solution_cypress/cypress/e2e/detection_response/detection_engine/rule_edit/indicator_match_rule.cy.ts b/x-pack/test/security_solution_cypress/cypress/e2e/detection_response/detection_engine/rule_edit/indicator_match_rule.cy.ts index 62d9a95398797..fe616f6ba1969 100644 --- a/x-pack/test/security_solution_cypress/cypress/e2e/detection_response/detection_engine/rule_edit/indicator_match_rule.cy.ts +++ b/x-pack/test/security_solution_cypress/cypress/e2e/detection_response/detection_engine/rule_edit/indicator_match_rule.cy.ts @@ -9,7 +9,6 @@ import { getNewThreatIndicatorRule } from '../../../../objects/rule'; import { SUPPRESS_FOR_DETAILS, - DETAILS_TITLE, SUPPRESS_BY_DETAILS, SUPPRESS_MISSING_FIELD, DEFINITION_DETAILS, @@ -81,9 +80,6 @@ describe( 'have.text', 'Suppress and group alerts for events with missing fields' ); - - // suppression functionality should be under Tech Preview - cy.contains(DETAILS_TITLE, SUPPRESS_FOR_DETAILS).contains('Technical Preview'); }); }); }); diff --git a/x-pack/test/security_solution_cypress/cypress/e2e/detection_response/detection_engine/rule_edit/machine_learning_rule.cy.ts b/x-pack/test/security_solution_cypress/cypress/e2e/detection_response/detection_engine/rule_edit/machine_learning_rule.cy.ts index e89e4b6afb817..7410d9fefae6d 100644 --- a/x-pack/test/security_solution_cypress/cypress/e2e/detection_response/detection_engine/rule_edit/machine_learning_rule.cy.ts +++ b/x-pack/test/security_solution_cypress/cypress/e2e/detection_response/detection_engine/rule_edit/machine_learning_rule.cy.ts @@ -13,7 +13,6 @@ import { } from '../../../../screens/create_new_rule'; import { DEFINITION_DETAILS, - DETAILS_TITLE, SUPPRESS_BY_DETAILS, SUPPRESS_FOR_DETAILS, SUPPRESS_MISSING_FIELD, @@ -88,9 +87,6 @@ describe( 'have.text', 'Suppress and group alerts for events with missing fields' ); - - // suppression functionality should be under Tech Preview - cy.contains(DETAILS_TITLE, SUPPRESS_FOR_DETAILS).contains('Technical Preview'); }); }); }); diff --git a/x-pack/test/security_solution_cypress/cypress/e2e/detection_response/detection_engine/rule_edit/preview.cy.ts b/x-pack/test/security_solution_cypress/cypress/e2e/detection_response/detection_engine/rule_edit/preview.cy.ts index ce298bafbfea0..268968c76ecc0 100644 --- a/x-pack/test/security_solution_cypress/cypress/e2e/detection_response/detection_engine/rule_edit/preview.cy.ts +++ b/x-pack/test/security_solution_cypress/cypress/e2e/detection_response/detection_engine/rule_edit/preview.cy.ts @@ -32,14 +32,7 @@ const expectedValidEsqlQuery = 'from auditbeat* METADATA _id'; describe( 'Detection rules, preview', { - // Currently FF are not supported on MKI environments, so this test should be skipped from MKI environments. - // Once `manualRuleRunEnabled` FF is removed, we can remove `@skipInServerlessMKI` as well - tags: ['@ess', '@serverless', '@skipInServerlessMKI'], - env: { - kbnServerArgs: [ - `--xpack.securitySolution.enableExperimental=${JSON.stringify(['loggingRequestsEnabled'])}`, - ], - }, + tags: ['@ess', '@serverless'], }, () => { beforeEach(() => { diff --git a/x-pack/test/security_solution_cypress/cypress/e2e/detection_response/detection_engine/rule_edit/threshold_rule.cy.ts b/x-pack/test/security_solution_cypress/cypress/e2e/detection_response/detection_engine/rule_edit/threshold_rule.cy.ts index 8d4bdf2d34976..dcc35a9e00080 100644 --- a/x-pack/test/security_solution_cypress/cypress/e2e/detection_response/detection_engine/rule_edit/threshold_rule.cy.ts +++ b/x-pack/test/security_solution_cypress/cypress/e2e/detection_response/detection_engine/rule_edit/threshold_rule.cy.ts @@ -9,7 +9,6 @@ import { getNewThresholdRule } from '../../../../objects/rule'; import { SUPPRESS_FOR_DETAILS, - DETAILS_TITLE, SUPPRESS_BY_DETAILS, SUPPRESS_MISSING_FIELD, } from '../../../../screens/rule_details'; @@ -63,8 +62,6 @@ describe( // ensure typed interval is displayed on details page getDetails(SUPPRESS_FOR_DETAILS).should('have.text', '60m'); - // suppression functionality should be under Tech Preview - cy.contains(DETAILS_TITLE, SUPPRESS_FOR_DETAILS).contains('Technical Preview'); // the rest of suppress properties do not exist for threshold rule assertDetailsNotExist(SUPPRESS_BY_DETAILS); diff --git a/x-pack/test/security_solution_cypress/cypress/e2e/detection_response/detection_engine/rule_gaps/bulk_manual_rule_run.cy.ts b/x-pack/test/security_solution_cypress/cypress/e2e/detection_response/detection_engine/rule_gaps/bulk_manual_rule_run.cy.ts index 17cde9485a13c..5a66dcdc0de84 100644 --- a/x-pack/test/security_solution_cypress/cypress/e2e/detection_response/detection_engine/rule_gaps/bulk_manual_rule_run.cy.ts +++ b/x-pack/test/security_solution_cypress/cypress/e2e/detection_response/detection_engine/rule_gaps/bulk_manual_rule_run.cy.ts @@ -19,9 +19,7 @@ import { deleteAlertsAndRules } from '../../../../tasks/api_calls/common'; import { createRule } from '../../../../tasks/api_calls/rules'; import { login } from '../../../../tasks/login'; -// Currently FF are not supported on MKI environments, so this test should be skipped from MKI environments. -// Once `manualRuleRunEnabled` FF is removed, we can remove `@skipInServerlessMKI` as well -describe('Manual rule run', { tags: ['@ess', '@serverless', '@skipInServerlessMKI'] }, () => { +describe('Manual rule run', { tags: ['@ess', '@serverless'] }, () => { beforeEach(() => { login(); deleteAlertsAndRules(); diff --git a/x-pack/test/security_solution_cypress/cypress/e2e/detection_response/detection_engine/rule_gaps/manual_rule_run.cy.ts b/x-pack/test/security_solution_cypress/cypress/e2e/detection_response/detection_engine/rule_gaps/manual_rule_run.cy.ts index 29e2379367c0b..f40f4284b84b5 100644 --- a/x-pack/test/security_solution_cypress/cypress/e2e/detection_response/detection_engine/rule_gaps/manual_rule_run.cy.ts +++ b/x-pack/test/security_solution_cypress/cypress/e2e/detection_response/detection_engine/rule_gaps/manual_rule_run.cy.ts @@ -18,9 +18,7 @@ import { deleteAlertsAndRules } from '../../../../tasks/api_calls/common'; import { createRule } from '../../../../tasks/api_calls/rules'; import { login } from '../../../../tasks/login'; -// Currently FF are not supported on MKI environments, so this test should be skipped from MKI environments. -// Once `manualRuleRunEnabled` FF is removed, we can remove `@skipInServerlessMKI` as well -describe('Manual rule run', { tags: ['@ess', '@serverless', '@skipInServerlessMKI'] }, () => { +describe('Manual rule run', { tags: ['@ess', '@serverless'] }, () => { beforeEach(() => { login(); deleteAlertsAndRules(); diff --git a/x-pack/test/security_solution_cypress/cypress/e2e/detection_response/rule_management/rule_details/backfill_group.cy.ts b/x-pack/test/security_solution_cypress/cypress/e2e/detection_response/rule_management/rule_details/backfill_group.cy.ts index 2f97e2f3c0721..6466c20dfde21 100644 --- a/x-pack/test/security_solution_cypress/cypress/e2e/detection_response/rule_management/rule_details/backfill_group.cy.ts +++ b/x-pack/test/security_solution_cypress/cypress/e2e/detection_response/rule_management/rule_details/backfill_group.cy.ts @@ -35,13 +35,6 @@ describe( 'Backfill groups', { tags: ['@ess', '@serverless', '@skipInServerlessMKI'], - env: { - ftrConfig: { - kbnServerArgs: [ - `--xpack.securitySolution.enableExperimental=${JSON.stringify(['manualRuleRunEnabled'])}`, - ], - }, - }, }, function () { before(() => { diff --git a/x-pack/test/security_solution_cypress/cypress/e2e/detection_response/rule_management/rule_details/execution_log.cy.ts b/x-pack/test/security_solution_cypress/cypress/e2e/detection_response/rule_management/rule_details/execution_log.cy.ts index a34826d2c8cb4..dc9e3e5719d27 100644 --- a/x-pack/test/security_solution_cypress/cypress/e2e/detection_response/rule_management/rule_details/execution_log.cy.ts +++ b/x-pack/test/security_solution_cypress/cypress/e2e/detection_response/rule_management/rule_details/execution_log.cy.ts @@ -27,13 +27,6 @@ describe.skip( 'Event log', { tags: ['@ess', '@serverless'], - env: { - ftrConfig: { - kbnServerArgs: [ - `--xpack.securitySolution.enableExperimental=${JSON.stringify(['manualRuleRunEnabled'])}`, - ], - }, - }, }, function () { before(() => { diff --git a/x-pack/test/security_solution_cypress/cypress/e2e/investigations/alerts/expandable_flyout/vulnerabilities_contextual_flyout.cy.ts b/x-pack/test/security_solution_cypress/cypress/e2e/investigations/alerts/expandable_flyout/vulnerabilities_contextual_flyout.cy.ts index 04ba10c908df7..fb83df1c79141 100644 --- a/x-pack/test/security_solution_cypress/cypress/e2e/investigations/alerts/expandable_flyout/vulnerabilities_contextual_flyout.cy.ts +++ b/x-pack/test/security_solution_cypress/cypress/e2e/investigations/alerts/expandable_flyout/vulnerabilities_contextual_flyout.cy.ts @@ -18,10 +18,12 @@ import { ALERTS_URL } from '../../../../urls/navigation'; import { visit } from '../../../../tasks/navigation'; const CSP_INSIGHT_VULNERABILITIES_TITLE = getDataTestSubjectSelector( - 'securitySolutionFlyoutInsightsVulnerabilitiesTitleText' + 'securitySolutionFlyoutInsightsVulnerabilitiesTitleLink' ); -const NO_VULNERABILITIES_TEXT = getDataTestSubjectSelector('noVulnerabilitiesDataTestSubj'); +const CSP_INSIGHT_VULNERABILITIES_TABLE = getDataTestSubjectSelector( + 'securitySolutionFlyoutVulnerabilitiesFindingsTable' +); const timestamp = Date.now(); @@ -136,7 +138,8 @@ const deleteDataStream = () => { }); }; -describe('Alert Host details expandable flyout', { tags: ['@ess', '@serverless'] }, () => { +// skipping because failure on MKI environment (https://buildkite.com/elastic/kibana-serverless-security-solution-quality-gate-investigations/builds/1390#01927579-caed-41bc-9440-3cf29629a263) +describe.skip('Alert Host details expandable flyout', { tags: ['@ess', '@serverless'] }, () => { beforeEach(() => { deleteAlertsAndRules(); login(); @@ -154,6 +157,28 @@ describe('Alert Host details expandable flyout', { tags: ['@ess', '@serverless'] }); }); + context( + 'Host name - Has Vulnerabilities findings but with different host name than the alerts', + () => { + beforeEach(() => { + createMockVulnerability(false); + cy.reload(); + expandFirstAlertHostFlyout(); + }); + + afterEach(() => { + deleteDataStream(); + }); + + it('should display Vulnerabilities preview under Insights Entities when it has Vulnerabilities Findings', () => { + expandFirstAlertHostFlyout(); + + cy.log('check if Vulnerabilities preview title is not shown'); + cy.get(CSP_INSIGHT_VULNERABILITIES_TITLE).should('not.exist'); + }); + } + ); + context('Host name - Has Vulnerabilities findings', () => { beforeEach(() => { createMockVulnerability(true); @@ -169,27 +194,10 @@ describe('Alert Host details expandable flyout', { tags: ['@ess', '@serverless'] cy.log('check if Vulnerabilities preview title shown'); cy.get(CSP_INSIGHT_VULNERABILITIES_TITLE).should('be.visible'); }); - }); - - context( - 'Host name - Has Vulnerabilities findings but host name is not the same as alert host name', - () => { - beforeEach(() => { - createMockVulnerability(false); - cy.reload(); - expandFirstAlertHostFlyout(); - }); - afterEach(() => { - deleteDataStream(); - }); - - it('should display Vulnerabilities preview under Insights Entities when it has Vulnerabilities Findings but it should show no vulnerabilities title', () => { - cy.log('check if Vulnerabilities preview title shown'); - cy.get(CSP_INSIGHT_VULNERABILITIES_TITLE).should('be.visible'); - cy.log('check if no vulnerabilities text is shown'); - cy.get(NO_VULNERABILITIES_TEXT).should('be.visible'); - }); - } - ); + it('should display insight tabs and findings table upon clicking on misconfiguration accordion', () => { + cy.get(CSP_INSIGHT_VULNERABILITIES_TITLE).click(); + cy.get(CSP_INSIGHT_VULNERABILITIES_TABLE).should('be.visible'); + }); + }); }); diff --git a/x-pack/test/security_solution_cypress/cypress/e2e/investigations/threat_intelligence/indicators.cy.ts b/x-pack/test/security_solution_cypress/cypress/e2e/investigations/threat_intelligence/indicators.cy.ts index ebfc5d4e9a0cb..b0e5764469459 100644 --- a/x-pack/test/security_solution_cypress/cypress/e2e/investigations/threat_intelligence/indicators.cy.ts +++ b/x-pack/test/security_solution_cypress/cypress/e2e/investigations/threat_intelligence/indicators.cy.ts @@ -298,7 +298,7 @@ describe('Multiple indicators', { tags: ['@ess'] }, () => { cy.log('should reload the data when refresh button is pressed'); - cy.intercept(/bsearch/).as('search'); + cy.intercept('POST', '/internal/search/threatIntelligenceSearchStrategy').as('search'); cy.get(REFRESH_BUTTON).should('exist').click(); cy.wait('@search'); }); diff --git a/x-pack/test/security_solution_cypress/cypress/tasks/create_new_rule.ts b/x-pack/test/security_solution_cypress/cypress/tasks/create_new_rule.ts index 68dc2cfffd908..501dd0461dd44 100644 --- a/x-pack/test/security_solution_cypress/cypress/tasks/create_new_rule.ts +++ b/x-pack/test/security_solution_cypress/cypress/tasks/create_new_rule.ts @@ -557,7 +557,7 @@ export const fillRuleActionFilters = (alertsFilter: AlertsFilter) => { .type(`{selectall}${alertsFilter.timeframe.timezone}{enter}`); }; -export const fillDefineThresholdRuleAndContinue = (rule: ThresholdRuleCreateProps) => { +export const fillDefineThresholdRule = (rule: ThresholdRuleCreateProps) => { const thresholdField = 0; const threshold = 1; @@ -578,7 +578,11 @@ export const fillDefineThresholdRuleAndContinue = (rule: ThresholdRuleCreateProp cy.wrap(inputs[threshold]).clear(); cy.wrap(inputs[threshold]).type(`${rule.threshold.value}`); }); - cy.get(DEFINE_CONTINUE_BUTTON).should('exist').click({ force: true }); +}; + +export const fillDefineThresholdRuleAndContinue = (rule: ThresholdRuleCreateProps) => { + fillDefineThresholdRule(rule); + continueFromDefineStep(); }; export const fillDefineEqlRule = (rule: EqlRuleCreateProps) => { @@ -908,6 +912,7 @@ export const enablesAndPopulatesThresholdSuppression = ( // enables suppression for threshold rule cy.get(THRESHOLD_ENABLE_SUPPRESSION_CHECKBOX).should('not.be.checked'); cy.get(THRESHOLD_ENABLE_SUPPRESSION_CHECKBOX).click(); + cy.get(THRESHOLD_ENABLE_SUPPRESSION_CHECKBOX).should('be.checked'); setAlertSuppressionDuration(interval, timeUnit); diff --git a/x-pack/test/security_solution_cypress/serverless_config.ts b/x-pack/test/security_solution_cypress/serverless_config.ts index 13877fcbf5af4..f3f04dda79dbb 100644 --- a/x-pack/test/security_solution_cypress/serverless_config.ts +++ b/x-pack/test/security_solution_cypress/serverless_config.ts @@ -34,10 +34,6 @@ export default async function ({ readConfigFile }: FtrConfigProviderContext) { { product_line: 'endpoint', product_tier: 'complete' }, { product_line: 'cloud', product_tier: 'complete' }, ])}`, - `--xpack.securitySolution.enableExperimental=${JSON.stringify([ - 'manualRuleRunEnabled', - 'loggingRequestsEnabled', - ])}`, '--csp.strict=false', '--csp.warnLegacyBrowsers=false', ], diff --git a/x-pack/test/spaces_api_integration/common/config.ts b/x-pack/test/spaces_api_integration/common/config.ts index f2074fac8bea1..466b34b65ce84 100644 --- a/x-pack/test/spaces_api_integration/common/config.ts +++ b/x-pack/test/spaces_api_integration/common/config.ts @@ -6,8 +6,9 @@ */ import path from 'path'; + import { REPO_ROOT } from '@kbn/repo-info'; -import { FtrConfigProviderContext } from '@kbn/test'; +import type { FtrConfigProviderContext } from '@kbn/test'; interface CreateTestConfigOptions { license: string; diff --git a/x-pack/test/spaces_api_integration/common/ftr_provider_context.d.ts b/x-pack/test/spaces_api_integration/common/ftr_provider_context.d.ts index aa56557c09df8..b05bbc8f6318d 100644 --- a/x-pack/test/spaces_api_integration/common/ftr_provider_context.d.ts +++ b/x-pack/test/spaces_api_integration/common/ftr_provider_context.d.ts @@ -5,8 +5,8 @@ * 2.0. */ -import { GenericFtrProviderContext } from '@kbn/test'; +import type { GenericFtrProviderContext } from '@kbn/test'; -import { services } from './services'; +import type { services } from './services'; export type FtrProviderContext = GenericFtrProviderContext; diff --git a/x-pack/test/spaces_api_integration/common/lib/create_users_and_roles.ts b/x-pack/test/spaces_api_integration/common/lib/create_users_and_roles.ts index b66c4a02a5bd6..2f93cc09fd032 100644 --- a/x-pack/test/spaces_api_integration/common/lib/create_users_and_roles.ts +++ b/x-pack/test/spaces_api_integration/common/lib/create_users_and_roles.ts @@ -5,8 +5,9 @@ * 2.0. */ -import { Agent as SuperTestAgent } from 'supertest'; import type { Client } from '@elastic/elasticsearch'; +import type { Agent as SuperTestAgent } from 'supertest'; + import { AUTHENTICATION } from './authentication'; export const createUsersAndRoles = async (es: Client, supertest: SuperTestAgent) => { diff --git a/x-pack/test/spaces_api_integration/common/lib/space_test_utils.ts b/x-pack/test/spaces_api_integration/common/lib/space_test_utils.ts index 8de4482474d8e..3c30208933550 100644 --- a/x-pack/test/spaces_api_integration/common/lib/space_test_utils.ts +++ b/x-pack/test/spaces_api_integration/common/lib/space_test_utils.ts @@ -6,6 +6,7 @@ */ import type { Client } from '@elastic/elasticsearch'; + import { ALL_SAVED_OBJECT_INDICES } from '@kbn/core-saved-objects-server'; import { DEFAULT_SPACE_ID } from '@kbn/spaces-plugin/common/constants'; diff --git a/x-pack/test/spaces_api_integration/common/plugins/spaces_test_plugin/server/plugin.ts b/x-pack/test/spaces_api_integration/common/plugins/spaces_test_plugin/server/plugin.ts index 07cc3bcd28c1b..56a226def6ba4 100644 --- a/x-pack/test/spaces_api_integration/common/plugins/spaces_test_plugin/server/plugin.ts +++ b/x-pack/test/spaces_api_integration/common/plugins/spaces_test_plugin/server/plugin.ts @@ -5,7 +5,7 @@ * 2.0. */ -import { CoreSetup } from '@kbn/core/server'; +import type { CoreSetup } from '@kbn/core/server'; export class Plugin { constructor() {} diff --git a/x-pack/test/spaces_api_integration/common/services.ts b/x-pack/test/spaces_api_integration/common/services.ts index 8b24d9b23e675..44ca7ec39c582 100644 --- a/x-pack/test/spaces_api_integration/common/services.ts +++ b/x-pack/test/spaces_api_integration/common/services.ts @@ -5,8 +5,8 @@ * 2.0. */ -import { services as commonServices } from '../../common/services'; import { services as apiIntegrationServices } from '../../api_integration/services'; +import { services as commonServices } from '../../common/services'; export const services = { ...commonServices, diff --git a/x-pack/test/spaces_api_integration/common/suites/copy_to_space.ts b/x-pack/test/spaces_api_integration/common/suites/copy_to_space.ts index 8de127aeaefe0..4cb2506977123 100644 --- a/x-pack/test/spaces_api_integration/common/suites/copy_to_space.ts +++ b/x-pack/test/spaces_api_integration/common/suites/copy_to_space.ts @@ -5,18 +5,20 @@ * 2.0. */ import type * as estypes from '@elastic/elasticsearch/lib/api/typesWithBodyKey'; -import expect from '@kbn/expect'; -import { DEFAULT_SPACE_ID } from '@kbn/spaces-plugin/common/constants'; -import { CopyResponse } from '@kbn/spaces-plugin/server/lib/copy_to_spaces'; -import { - SavedObjectsImportFailure, +import type { SuperTest } from 'supertest'; + +import type { SavedObjectsImportAmbiguousConflictError, + SavedObjectsImportFailure, } from '@kbn/core/server'; -import { SuperTest } from 'supertest'; -import { getAggregatedSpaceData, getUrlPrefix } from '../lib/space_test_utils'; -import { DescribeFn, TestDefinitionAuthentication } from '../lib/types'; +import expect from '@kbn/expect'; +import { DEFAULT_SPACE_ID } from '@kbn/spaces-plugin/common/constants'; +import type { CopyResponse } from '@kbn/spaces-plugin/server/lib/copy_to_spaces'; + import { getTestDataLoader, SPACE_1, SPACE_2 } from '../../../common/lib/test_data_loader'; import type { FtrProviderContext } from '../ftr_provider_context'; +import { getAggregatedSpaceData, getUrlPrefix } from '../lib/space_test_utils'; +import type { DescribeFn, TestDefinitionAuthentication } from '../lib/types'; type TestResponse = Record; diff --git a/x-pack/test/spaces_api_integration/common/suites/create.ts b/x-pack/test/spaces_api_integration/common/suites/create.ts index ce3113ec9639c..3c65ba8aba156 100644 --- a/x-pack/test/spaces_api_integration/common/suites/create.ts +++ b/x-pack/test/spaces_api_integration/common/suites/create.ts @@ -5,10 +5,12 @@ * 2.0. */ +import type { SuperTest } from 'supertest'; + import expect from '@kbn/expect'; -import { SuperTest } from 'supertest'; + import { getTestScenariosForSpace } from '../lib/space_test_utils'; -import { DescribeFn, TestDefinitionAuthentication } from '../lib/types'; +import type { DescribeFn, TestDefinitionAuthentication } from '../lib/types'; interface CreateTest { statusCode: number; diff --git a/x-pack/test/spaces_api_integration/common/suites/delete.ts b/x-pack/test/spaces_api_integration/common/suites/delete.ts index fd04b79fd1ef5..1a75bc219ae1f 100644 --- a/x-pack/test/spaces_api_integration/common/suites/delete.ts +++ b/x-pack/test/spaces_api_integration/common/suites/delete.ts @@ -5,13 +5,15 @@ * 2.0. */ -import expect from '@kbn/expect'; -import { SuperTest } from 'supertest'; import type { Client } from '@elastic/elasticsearch'; +import type { SuperTest } from 'supertest'; + import { ALL_SAVED_OBJECT_INDICES } from '@kbn/core-saved-objects-server'; -import { getAggregatedSpaceData, getTestScenariosForSpace } from '../lib/space_test_utils'; +import expect from '@kbn/expect'; + import { MULTI_NAMESPACE_SAVED_OBJECT_TEST_CASES as CASES } from '../lib/saved_object_test_cases'; -import { DescribeFn, TestDefinitionAuthentication } from '../lib/types'; +import { getAggregatedSpaceData, getTestScenariosForSpace } from '../lib/space_test_utils'; +import type { DescribeFn, TestDefinitionAuthentication } from '../lib/types'; interface DeleteTest { statusCode: number; diff --git a/x-pack/test/spaces_api_integration/common/suites/disable_legacy_url_aliases.ts b/x-pack/test/spaces_api_integration/common/suites/disable_legacy_url_aliases.ts index 5889a10479f31..a00729e7eca8c 100644 --- a/x-pack/test/spaces_api_integration/common/suites/disable_legacy_url_aliases.ts +++ b/x-pack/test/spaces_api_integration/common/suites/disable_legacy_url_aliases.ts @@ -5,18 +5,20 @@ * 2.0. */ -import expect from '@kbn/expect'; -import type { Agent as SuperTestAgent } from 'supertest'; import type { Client } from '@elastic/elasticsearch'; +import type { Agent as SuperTestAgent } from 'supertest'; + import type { LegacyUrlAlias } from '@kbn/core-saved-objects-base-server-internal'; import { MAIN_SAVED_OBJECT_INDEX } from '@kbn/core-saved-objects-server'; -import { SPACES } from '../lib/spaces'; +import expect from '@kbn/expect'; + import { getUrlPrefix } from '../../../saved_object_api_integration/common/lib/saved_object_test_utils'; import type { ExpectResponseBody, TestDefinition, TestSuite, } from '../../../saved_object_api_integration/common/lib/types'; +import { SPACES } from '../lib/spaces'; export interface DisableLegacyUrlAliasesTestDefinition extends TestDefinition { request: { diff --git a/x-pack/test/spaces_api_integration/common/suites/get.ts b/x-pack/test/spaces_api_integration/common/suites/get.ts index a733bd21c5fc2..cb3879591ed25 100644 --- a/x-pack/test/spaces_api_integration/common/suites/get.ts +++ b/x-pack/test/spaces_api_integration/common/suites/get.ts @@ -5,10 +5,12 @@ * 2.0. */ +import type { SuperAgent } from 'superagent'; + import expect from '@kbn/expect'; -import { SuperAgent } from 'superagent'; + import { getTestScenariosForSpace } from '../lib/space_test_utils'; -import { DescribeFn, TestDefinitionAuthentication } from '../lib/types'; +import type { DescribeFn, TestDefinitionAuthentication } from '../lib/types'; interface GetTest { statusCode: number; diff --git a/x-pack/test/spaces_api_integration/common/suites/get_all.ts b/x-pack/test/spaces_api_integration/common/suites/get_all.ts index 88625c3d9b51e..236c98d9364b9 100644 --- a/x-pack/test/spaces_api_integration/common/suites/get_all.ts +++ b/x-pack/test/spaces_api_integration/common/suites/get_all.ts @@ -5,10 +5,12 @@ * 2.0. */ +import type { SuperTest } from 'supertest'; + import expect from '@kbn/expect'; -import { SuperTest } from 'supertest'; + import { getTestScenariosForSpace } from '../lib/space_test_utils'; -import { DescribeFn, TestDefinitionAuthentication } from '../lib/types'; +import type { DescribeFn, TestDefinitionAuthentication } from '../lib/types'; interface GetAllTest { statusCode: number; diff --git a/x-pack/test/spaces_api_integration/common/suites/get_shareable_references.ts b/x-pack/test/spaces_api_integration/common/suites/get_shareable_references.ts index c1630bc288169..553aedbab958b 100644 --- a/x-pack/test/spaces_api_integration/common/suites/get_shareable_references.ts +++ b/x-pack/test/spaces_api_integration/common/suites/get_shareable_references.ts @@ -5,24 +5,26 @@ * 2.0. */ -import expect from '@kbn/expect'; -import { deepFreeze } from '@kbn/std'; -import { Agent as SuperTestAgent } from 'supertest'; -import { - SavedObjectsCollectMultiNamespaceReferencesResponse, +import type { Agent as SuperTestAgent } from 'supertest'; + +import type { SavedObjectReferenceWithContext, + SavedObjectsCollectMultiNamespaceReferencesResponse, } from '@kbn/core/server'; -import { MULTI_NAMESPACE_SAVED_OBJECT_TEST_CASES as CASES } from '../lib/saved_object_test_cases'; -import { SPACES } from '../lib/spaces'; +import expect from '@kbn/expect'; +import { deepFreeze } from '@kbn/std'; + import { expectResponses, getUrlPrefix, } from '../../../saved_object_api_integration/common/lib/saved_object_test_utils'; -import { +import type { ExpectResponseBody, TestDefinition, TestSuite, } from '../../../saved_object_api_integration/common/lib/types'; +import { MULTI_NAMESPACE_SAVED_OBJECT_TEST_CASES as CASES } from '../lib/saved_object_test_cases'; +import { SPACES } from '../lib/spaces'; export interface GetShareableReferencesTestDefinition extends TestDefinition { request: { diff --git a/x-pack/test/spaces_api_integration/common/suites/resolve_copy_to_space_conflicts.ts b/x-pack/test/spaces_api_integration/common/suites/resolve_copy_to_space_conflicts.ts index 4c3e9c834a0f5..e07d56a95ba24 100644 --- a/x-pack/test/spaces_api_integration/common/suites/resolve_copy_to_space_conflicts.ts +++ b/x-pack/test/spaces_api_integration/common/suites/resolve_copy_to_space_conflicts.ts @@ -5,15 +5,17 @@ * 2.0. */ +import type { SuperTest } from 'supertest'; + +import type { SavedObject } from '@kbn/core/server'; import expect from '@kbn/expect'; -import { SavedObject } from '@kbn/core/server'; import { DEFAULT_SPACE_ID } from '@kbn/spaces-plugin/common/constants'; -import { CopyResponse } from '@kbn/spaces-plugin/server/lib/copy_to_spaces'; -import { SuperTest } from 'supertest'; -import { getUrlPrefix } from '../lib/space_test_utils'; -import { DescribeFn, TestDefinitionAuthentication } from '../lib/types'; -import type { FtrProviderContext } from '../ftr_provider_context'; +import type { CopyResponse } from '@kbn/spaces-plugin/server/lib/copy_to_spaces'; + import { getTestDataLoader, SPACE_1, SPACE_2 } from '../../../common/lib/test_data_loader'; +import type { FtrProviderContext } from '../ftr_provider_context'; +import { getUrlPrefix } from '../lib/space_test_utils'; +import type { DescribeFn, TestDefinitionAuthentication } from '../lib/types'; type TestResponse = Record; diff --git a/x-pack/test/spaces_api_integration/common/suites/update.ts b/x-pack/test/spaces_api_integration/common/suites/update.ts index 41e5d97f20514..62226bf4dbb8d 100644 --- a/x-pack/test/spaces_api_integration/common/suites/update.ts +++ b/x-pack/test/spaces_api_integration/common/suites/update.ts @@ -5,10 +5,12 @@ * 2.0. */ +import type { SuperTest } from 'supertest'; + import expect from '@kbn/expect'; -import { SuperTest } from 'supertest'; + import { getUrlPrefix } from '../lib/space_test_utils'; -import { DescribeFn, TestDefinitionAuthentication } from '../lib/types'; +import type { DescribeFn, TestDefinitionAuthentication } from '../lib/types'; interface UpdateTest { statusCode: number; diff --git a/x-pack/test/spaces_api_integration/common/suites/update_objects_spaces.ts b/x-pack/test/spaces_api_integration/common/suites/update_objects_spaces.ts index 777581d9aa5a0..05ed64e1e6047 100644 --- a/x-pack/test/spaces_api_integration/common/suites/update_objects_spaces.ts +++ b/x-pack/test/spaces_api_integration/common/suites/update_objects_spaces.ts @@ -5,26 +5,26 @@ * 2.0. */ -import expect from '@kbn/expect'; import type { Client } from '@elastic/elasticsearch'; import type { SearchTotalHits } from '@elastic/elasticsearch/lib/api/types'; -import { without, uniq } from 'lodash'; -import { Agent as SuperTestAgent } from 'supertest'; -import { - SavedObjectsErrorHelpers, - SavedObjectsUpdateObjectsSpacesResponse, -} from '@kbn/core/server'; +import { uniq, without } from 'lodash'; +import type { Agent as SuperTestAgent } from 'supertest'; + +import type { SavedObjectsUpdateObjectsSpacesResponse } from '@kbn/core/server'; +import { SavedObjectsErrorHelpers } from '@kbn/core/server'; import { ALL_SAVED_OBJECT_INDICES } from '@kbn/core-saved-objects-server'; -import { SPACES } from '../lib/spaces'; +import expect from '@kbn/expect'; + import { expectResponses, getUrlPrefix, } from '../../../saved_object_api_integration/common/lib/saved_object_test_utils'; -import { +import type { ExpectResponseBody, TestDefinition, TestSuite, } from '../../../saved_object_api_integration/common/lib/types'; +import { SPACES } from '../lib/spaces'; export interface UpdateObjectsSpacesTestDefinition extends TestDefinition { request: { diff --git a/x-pack/test/spaces_api_integration/security_and_spaces/apis/copy_to_space/copy_to_space.ts b/x-pack/test/spaces_api_integration/security_and_spaces/apis/copy_to_space/copy_to_space.ts index 90b2cc67439d7..b7e10cb500e80 100644 --- a/x-pack/test/spaces_api_integration/security_and_spaces/apis/copy_to_space/copy_to_space.ts +++ b/x-pack/test/spaces_api_integration/security_and_spaces/apis/copy_to_space/copy_to_space.ts @@ -5,10 +5,10 @@ * 2.0. */ +import type { FtrProviderContext } from '../../../common/ftr_provider_context'; import { AUTHENTICATION } from '../../../common/lib/authentication'; import { SPACES } from '../../../common/lib/spaces'; import { copyToSpaceTestSuiteFactory } from '../../../common/suites/copy_to_space'; -import { FtrProviderContext } from '../../../common/ftr_provider_context'; // eslint-disable-next-line import/no-default-export export default function copyToSpaceSpacesAndSecuritySuite(context: FtrProviderContext) { diff --git a/x-pack/test/spaces_api_integration/security_and_spaces/apis/copy_to_space/index.ts b/x-pack/test/spaces_api_integration/security_and_spaces/apis/copy_to_space/index.ts index c1edb1e5e5ac1..79a6e69cb6f32 100644 --- a/x-pack/test/spaces_api_integration/security_and_spaces/apis/copy_to_space/index.ts +++ b/x-pack/test/spaces_api_integration/security_and_spaces/apis/copy_to_space/index.ts @@ -5,8 +5,8 @@ * 2.0. */ +import type { FtrProviderContext } from '../../../common/ftr_provider_context'; import { createUsersAndRoles } from '../../../common/lib/create_users_and_roles'; -import { FtrProviderContext } from '../../../common/ftr_provider_context'; // eslint-disable-next-line import/no-default-export export default function ({ loadTestFile, getService }: FtrProviderContext) { diff --git a/x-pack/test/spaces_api_integration/security_and_spaces/apis/create.ts b/x-pack/test/spaces_api_integration/security_and_spaces/apis/create.ts index 3195f35c734cb..6e5bc1649cb3f 100644 --- a/x-pack/test/spaces_api_integration/security_and_spaces/apis/create.ts +++ b/x-pack/test/spaces_api_integration/security_and_spaces/apis/create.ts @@ -5,11 +5,12 @@ * 2.0. */ -import { SuperTest } from 'supertest'; +import type { SuperTest } from 'supertest'; + +import type { FtrProviderContext } from '../../common/ftr_provider_context'; import { AUTHENTICATION } from '../../common/lib/authentication'; import { SPACES } from '../../common/lib/spaces'; import { createTestSuiteFactory } from '../../common/suites/create'; -import { FtrProviderContext } from '../../common/ftr_provider_context'; // eslint-disable-next-line import/no-default-export export default function createSpacesOnlySuite({ getService }: FtrProviderContext) { diff --git a/x-pack/test/spaces_api_integration/security_and_spaces/apis/delete.ts b/x-pack/test/spaces_api_integration/security_and_spaces/apis/delete.ts index 60bcb91125858..01fd306a7f558 100644 --- a/x-pack/test/spaces_api_integration/security_and_spaces/apis/delete.ts +++ b/x-pack/test/spaces_api_integration/security_and_spaces/apis/delete.ts @@ -5,11 +5,12 @@ * 2.0. */ -import { SuperTest } from 'supertest'; +import type { SuperTest } from 'supertest'; + +import type { FtrProviderContext } from '../../common/ftr_provider_context'; import { AUTHENTICATION } from '../../common/lib/authentication'; import { SPACES } from '../../common/lib/spaces'; import { deleteTestSuiteFactory } from '../../common/suites/delete'; -import { FtrProviderContext } from '../../common/ftr_provider_context'; // eslint-disable-next-line import/no-default-export export default function deleteSpaceTestSuite({ getService }: FtrProviderContext) { diff --git a/x-pack/test/spaces_api_integration/security_and_spaces/apis/disable_legacy_url_aliases.ts b/x-pack/test/spaces_api_integration/security_and_spaces/apis/disable_legacy_url_aliases.ts index cd00a3d8b7ee6..2498ce166bcce 100644 --- a/x-pack/test/spaces_api_integration/security_and_spaces/apis/disable_legacy_url_aliases.ts +++ b/x-pack/test/spaces_api_integration/security_and_spaces/apis/disable_legacy_url_aliases.ts @@ -5,16 +5,16 @@ * 2.0. */ -import { SPACES } from '../../common/lib/spaces'; import { getTestScenarios } from '../../../saved_object_api_integration/common/lib/saved_object_test_utils'; -import { TestUser } from '../../../saved_object_api_integration/common/lib/types'; +import type { TestUser } from '../../../saved_object_api_integration/common/lib/types'; +import type { FtrProviderContext } from '../../common/ftr_provider_context'; +import { SPACES } from '../../common/lib/spaces'; +import type { DisableLegacyUrlAliasesTestDefinition } from '../../common/suites/disable_legacy_url_aliases'; import { disableLegacyUrlAliasesTestSuiteFactory, - TEST_CASE_TARGET_TYPE, TEST_CASE_SOURCE_ID, - DisableLegacyUrlAliasesTestDefinition, + TEST_CASE_TARGET_TYPE, } from '../../common/suites/disable_legacy_url_aliases'; -import { FtrProviderContext } from '../../common/ftr_provider_context'; const { DEFAULT: { spaceId: DEFAULT_SPACE_ID }, diff --git a/x-pack/test/spaces_api_integration/security_and_spaces/apis/get.ts b/x-pack/test/spaces_api_integration/security_and_spaces/apis/get.ts index 7354138b7987e..cc24ffa0d891a 100644 --- a/x-pack/test/spaces_api_integration/security_and_spaces/apis/get.ts +++ b/x-pack/test/spaces_api_integration/security_and_spaces/apis/get.ts @@ -5,11 +5,12 @@ * 2.0. */ -import { SuperTest } from 'supertest'; +import type { SuperTest } from 'supertest'; + +import type { FtrProviderContext } from '../../common/ftr_provider_context'; import { AUTHENTICATION } from '../../common/lib/authentication'; import { SPACES } from '../../common/lib/spaces'; import { getTestSuiteFactory } from '../../common/suites/get'; -import { FtrProviderContext } from '../../common/ftr_provider_context'; // eslint-disable-next-line import/no-default-export export default function getSpaceTestSuite({ getService }: FtrProviderContext) { diff --git a/x-pack/test/spaces_api_integration/security_and_spaces/apis/get_all.ts b/x-pack/test/spaces_api_integration/security_and_spaces/apis/get_all.ts index d2c3b8be03be2..1c2db5f6bcd7c 100644 --- a/x-pack/test/spaces_api_integration/security_and_spaces/apis/get_all.ts +++ b/x-pack/test/spaces_api_integration/security_and_spaces/apis/get_all.ts @@ -5,11 +5,12 @@ * 2.0. */ -import { SuperTest } from 'supertest'; +import type { SuperTest } from 'supertest'; + +import type { FtrProviderContext } from '../../common/ftr_provider_context'; import { AUTHENTICATION } from '../../common/lib/authentication'; import { SPACES } from '../../common/lib/spaces'; import { getAllTestSuiteFactory } from '../../common/suites/get_all'; -import { FtrProviderContext } from '../../common/ftr_provider_context'; // eslint-disable-next-line import/no-default-export export default function getAllSpacesTestSuite({ getService }: FtrProviderContext) { diff --git a/x-pack/test/spaces_api_integration/security_and_spaces/apis/get_shareable_references.ts b/x-pack/test/spaces_api_integration/security_and_spaces/apis/get_shareable_references.ts index d3466dd511e82..093e01dd8fbbf 100644 --- a/x-pack/test/spaces_api_integration/security_and_spaces/apis/get_shareable_references.ts +++ b/x-pack/test/spaces_api_integration/security_and_spaces/apis/get_shareable_references.ts @@ -5,17 +5,19 @@ * 2.0. */ -import { SPACES } from '../../common/lib/spaces'; import { getTestScenarios } from '../../../saved_object_api_integration/common/lib/saved_object_test_utils'; -import { TestUser } from '../../../saved_object_api_integration/common/lib/types'; -import { - getShareableReferencesTestSuiteFactory, +import type { TestUser } from '../../../saved_object_api_integration/common/lib/types'; +import type { FtrProviderContext } from '../../common/ftr_provider_context'; +import { SPACES } from '../../common/lib/spaces'; +import type { GetShareableReferencesTestCase, GetShareableReferencesTestDefinition, - TEST_CASE_OBJECTS, +} from '../../common/suites/get_shareable_references'; +import { EXPECTED_RESULTS, + getShareableReferencesTestSuiteFactory, + TEST_CASE_OBJECTS, } from '../../common/suites/get_shareable_references'; -import { FtrProviderContext } from '../../common/ftr_provider_context'; const { DEFAULT: { spaceId: DEFAULT_SPACE_ID }, 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 ae5f7b48a2809..756d47308f7fc 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 @@ -5,8 +5,8 @@ * 2.0. */ +import type { FtrProviderContext } from '../../common/ftr_provider_context'; import { createUsersAndRoles } from '../../common/lib/create_users_and_roles'; -import { FtrProviderContext } from '../../common/ftr_provider_context'; // eslint-disable-next-line import/no-default-export export default function ({ loadTestFile, getService }: FtrProviderContext) { diff --git a/x-pack/test/spaces_api_integration/security_and_spaces/apis/resolve_copy_to_space_conflicts.ts b/x-pack/test/spaces_api_integration/security_and_spaces/apis/resolve_copy_to_space_conflicts.ts index 2f1788ae348f9..da5054436dc69 100644 --- a/x-pack/test/spaces_api_integration/security_and_spaces/apis/resolve_copy_to_space_conflicts.ts +++ b/x-pack/test/spaces_api_integration/security_and_spaces/apis/resolve_copy_to_space_conflicts.ts @@ -5,10 +5,10 @@ * 2.0. */ +import type { FtrProviderContext } from '../../common/ftr_provider_context'; import { AUTHENTICATION } from '../../common/lib/authentication'; import { SPACES } from '../../common/lib/spaces'; import { resolveCopyToSpaceConflictsSuite } from '../../common/suites/resolve_copy_to_space_conflicts'; -import { FtrProviderContext } from '../../common/ftr_provider_context'; // eslint-disable-next-line import/no-default-export export default function resolveCopyToSpaceConflictsTestSuite(context: FtrProviderContext) { diff --git a/x-pack/test/spaces_api_integration/security_and_spaces/apis/update.ts b/x-pack/test/spaces_api_integration/security_and_spaces/apis/update.ts index 40ddc6549edcc..57fd1c5b1a202 100644 --- a/x-pack/test/spaces_api_integration/security_and_spaces/apis/update.ts +++ b/x-pack/test/spaces_api_integration/security_and_spaces/apis/update.ts @@ -5,11 +5,12 @@ * 2.0. */ -import { SuperTest } from 'supertest'; +import type { SuperTest } from 'supertest'; + +import type { FtrProviderContext } from '../../common/ftr_provider_context'; import { AUTHENTICATION } from '../../common/lib/authentication'; import { SPACES } from '../../common/lib/spaces'; import { updateTestSuiteFactory } from '../../common/suites/update'; -import { FtrProviderContext } from '../../common/ftr_provider_context'; // eslint-disable-next-line import/no-default-export export default function updateSpaceTestSuite({ getService }: FtrProviderContext) { diff --git a/x-pack/test/spaces_api_integration/security_and_spaces/apis/update_objects_spaces.ts b/x-pack/test/spaces_api_integration/security_and_spaces/apis/update_objects_spaces.ts index c6a97337e6ad9..ab7e4db5908f6 100644 --- a/x-pack/test/spaces_api_integration/security_and_spaces/apis/update_objects_spaces.ts +++ b/x-pack/test/spaces_api_integration/security_and_spaces/apis/update_objects_spaces.ts @@ -5,19 +5,19 @@ * 2.0. */ -import { SPACES } from '../../common/lib/spaces'; import { - testCaseFailures, getTestScenarios, + testCaseFailures, } from '../../../saved_object_api_integration/common/lib/saved_object_test_utils'; -import { TestUser } from '../../../saved_object_api_integration/common/lib/types'; +import type { TestUser } from '../../../saved_object_api_integration/common/lib/types'; +import type { FtrProviderContext } from '../../common/ftr_provider_context'; import { MULTI_NAMESPACE_SAVED_OBJECT_TEST_CASES as CASES } from '../../common/lib/saved_object_test_cases'; -import { - updateObjectsSpacesTestSuiteFactory, - UpdateObjectsSpacesTestDefinition, +import { SPACES } from '../../common/lib/spaces'; +import type { UpdateObjectsSpacesTestCase, + UpdateObjectsSpacesTestDefinition, } from '../../common/suites/update_objects_spaces'; -import { FtrProviderContext } from '../../common/ftr_provider_context'; +import { updateObjectsSpacesTestSuiteFactory } from '../../common/suites/update_objects_spaces'; const { DEFAULT: { spaceId: DEFAULT_SPACE_ID }, diff --git a/x-pack/test/spaces_api_integration/spaces_only/apis/copy_to_space.ts b/x-pack/test/spaces_api_integration/spaces_only/apis/copy_to_space.ts index 4139e94610f08..3867d528ef374 100644 --- a/x-pack/test/spaces_api_integration/spaces_only/apis/copy_to_space.ts +++ b/x-pack/test/spaces_api_integration/spaces_only/apis/copy_to_space.ts @@ -5,7 +5,7 @@ * 2.0. */ -import { FtrProviderContext } from '../../common/ftr_provider_context'; +import type { FtrProviderContext } from '../../common/ftr_provider_context'; import { copyToSpaceTestSuiteFactory } from '../../common/suites/copy_to_space'; // eslint-disable-next-line import/no-default-export diff --git a/x-pack/test/spaces_api_integration/spaces_only/apis/create.ts b/x-pack/test/spaces_api_integration/spaces_only/apis/create.ts index eea6204267bdd..c98754d60eec3 100644 --- a/x-pack/test/spaces_api_integration/spaces_only/apis/create.ts +++ b/x-pack/test/spaces_api_integration/spaces_only/apis/create.ts @@ -5,10 +5,11 @@ * 2.0. */ -import { SuperTest } from 'supertest'; +import type { SuperTest } from 'supertest'; + +import type { FtrProviderContext } from '../../common/ftr_provider_context'; import { SPACES } from '../../common/lib/spaces'; import { createTestSuiteFactory } from '../../common/suites/create'; -import { FtrProviderContext } from '../../common/ftr_provider_context'; // eslint-disable-next-line import/no-default-export export default function createSpacesOnlySuite({ getService }: FtrProviderContext) { diff --git a/x-pack/test/spaces_api_integration/spaces_only/apis/delete.ts b/x-pack/test/spaces_api_integration/spaces_only/apis/delete.ts index f066018744818..da50b9c37f190 100644 --- a/x-pack/test/spaces_api_integration/spaces_only/apis/delete.ts +++ b/x-pack/test/spaces_api_integration/spaces_only/apis/delete.ts @@ -5,10 +5,11 @@ * 2.0. */ -import { SuperTest } from 'supertest'; +import type { SuperTest } from 'supertest'; + +import type { FtrProviderContext } from '../../common/ftr_provider_context'; import { SPACES } from '../../common/lib/spaces'; import { deleteTestSuiteFactory } from '../../common/suites/delete'; -import { FtrProviderContext } from '../../common/ftr_provider_context'; // eslint-disable-next-line import/no-default-export export default function deleteSpaceTestSuite({ getService }: FtrProviderContext) { diff --git a/x-pack/test/spaces_api_integration/spaces_only/apis/disable_legacy_url_aliases.ts b/x-pack/test/spaces_api_integration/spaces_only/apis/disable_legacy_url_aliases.ts index 32e774e2de636..1818beef05118 100644 --- a/x-pack/test/spaces_api_integration/spaces_only/apis/disable_legacy_url_aliases.ts +++ b/x-pack/test/spaces_api_integration/spaces_only/apis/disable_legacy_url_aliases.ts @@ -5,14 +5,14 @@ * 2.0. */ +import type { FtrProviderContext } from '../../common/ftr_provider_context'; import { SPACES } from '../../common/lib/spaces'; +import type { DisableLegacyUrlAliasesTestCase } from '../../common/suites/disable_legacy_url_aliases'; import { disableLegacyUrlAliasesTestSuiteFactory, - DisableLegacyUrlAliasesTestCase, - TEST_CASE_TARGET_TYPE, TEST_CASE_SOURCE_ID, + TEST_CASE_TARGET_TYPE, } from '../../common/suites/disable_legacy_url_aliases'; -import { FtrProviderContext } from '../../common/ftr_provider_context'; const { DEFAULT: { spaceId: DEFAULT_SPACE_ID }, diff --git a/x-pack/test/spaces_api_integration/spaces_only/apis/get.ts b/x-pack/test/spaces_api_integration/spaces_only/apis/get.ts index e7f9acc06b655..5cca880f2a7f4 100644 --- a/x-pack/test/spaces_api_integration/spaces_only/apis/get.ts +++ b/x-pack/test/spaces_api_integration/spaces_only/apis/get.ts @@ -5,10 +5,11 @@ * 2.0. */ -import { SuperTest } from 'supertest'; +import type { SuperTest } from 'supertest'; + +import type { FtrProviderContext } from '../../common/ftr_provider_context'; import { SPACES } from '../../common/lib/spaces'; import { getTestSuiteFactory } from '../../common/suites/get'; -import { FtrProviderContext } from '../../common/ftr_provider_context'; // eslint-disable-next-line import/no-default-export export default function getSpaceTestSuite({ getService }: FtrProviderContext) { diff --git a/x-pack/test/spaces_api_integration/spaces_only/apis/get_all.ts b/x-pack/test/spaces_api_integration/spaces_only/apis/get_all.ts index 6331c843649fa..4b52f25eecbea 100644 --- a/x-pack/test/spaces_api_integration/spaces_only/apis/get_all.ts +++ b/x-pack/test/spaces_api_integration/spaces_only/apis/get_all.ts @@ -5,10 +5,11 @@ * 2.0. */ -import { SuperTest } from 'supertest'; +import type { SuperTest } from 'supertest'; + +import type { FtrProviderContext } from '../../common/ftr_provider_context'; import { SPACES } from '../../common/lib/spaces'; import { getAllTestSuiteFactory } from '../../common/suites/get_all'; -import { FtrProviderContext } from '../../common/ftr_provider_context'; // eslint-disable-next-line import/no-default-export export default function getAllSpacesTestSuite({ getService }: FtrProviderContext) { diff --git a/x-pack/test/spaces_api_integration/spaces_only/apis/get_shareable_references.ts b/x-pack/test/spaces_api_integration/spaces_only/apis/get_shareable_references.ts index 5eec1dda83e5a..15fef24ad0d69 100644 --- a/x-pack/test/spaces_api_integration/spaces_only/apis/get_shareable_references.ts +++ b/x-pack/test/spaces_api_integration/spaces_only/apis/get_shareable_references.ts @@ -5,15 +5,15 @@ * 2.0. */ -import { SPACES } from '../../common/lib/spaces'; import { getTestScenarios } from '../../../saved_object_api_integration/common/lib/saved_object_test_utils'; +import type { FtrProviderContext } from '../../common/ftr_provider_context'; +import { SPACES } from '../../common/lib/spaces'; +import type { GetShareableReferencesTestCase } from '../../common/suites/get_shareable_references'; import { + EXPECTED_RESULTS, getShareableReferencesTestSuiteFactory, - GetShareableReferencesTestCase, TEST_CASE_OBJECTS, - EXPECTED_RESULTS, } from '../../common/suites/get_shareable_references'; -import { FtrProviderContext } from '../../common/ftr_provider_context'; const { DEFAULT: { spaceId: DEFAULT_SPACE_ID }, 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 50c97b9ca0a75..433ce2c6c444c 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 @@ -5,7 +5,7 @@ * 2.0. */ -import { FtrProviderContext } from '../../common/ftr_provider_context'; +import type { FtrProviderContext } from '../../common/ftr_provider_context'; // eslint-disable-next-line import/no-default-export export default function spacesOnlyTestSuite({ loadTestFile }: FtrProviderContext) { diff --git a/x-pack/test/spaces_api_integration/spaces_only/apis/resolve_copy_to_space_conflicts.ts b/x-pack/test/spaces_api_integration/spaces_only/apis/resolve_copy_to_space_conflicts.ts index 2248c67cd8219..9fc16ed64abb8 100644 --- a/x-pack/test/spaces_api_integration/spaces_only/apis/resolve_copy_to_space_conflicts.ts +++ b/x-pack/test/spaces_api_integration/spaces_only/apis/resolve_copy_to_space_conflicts.ts @@ -5,7 +5,7 @@ * 2.0. */ -import { FtrProviderContext } from '../../common/ftr_provider_context'; +import type { FtrProviderContext } from '../../common/ftr_provider_context'; import { resolveCopyToSpaceConflictsSuite } from '../../common/suites/resolve_copy_to_space_conflicts'; // eslint-disable-next-line import/no-default-export diff --git a/x-pack/test/spaces_api_integration/spaces_only/apis/update.ts b/x-pack/test/spaces_api_integration/spaces_only/apis/update.ts index 40e4df87e74e9..4a4055896e546 100644 --- a/x-pack/test/spaces_api_integration/spaces_only/apis/update.ts +++ b/x-pack/test/spaces_api_integration/spaces_only/apis/update.ts @@ -5,10 +5,11 @@ * 2.0. */ -import { SuperTest } from 'supertest'; +import type { SuperTest } from 'supertest'; + +import type { FtrProviderContext } from '../../common/ftr_provider_context'; import { SPACES } from '../../common/lib/spaces'; import { updateTestSuiteFactory } from '../../common/suites/update'; -import { FtrProviderContext } from '../../common/ftr_provider_context'; // eslint-disable-next-line import/no-default-export export default function updateSpaceTestSuite({ getService }: FtrProviderContext) { diff --git a/x-pack/test/spaces_api_integration/spaces_only/apis/update_objects_spaces.ts b/x-pack/test/spaces_api_integration/spaces_only/apis/update_objects_spaces.ts index a8c2bdce2a3a5..c45aa9edc6741 100644 --- a/x-pack/test/spaces_api_integration/spaces_only/apis/update_objects_spaces.ts +++ b/x-pack/test/spaces_api_integration/spaces_only/apis/update_objects_spaces.ts @@ -5,15 +5,15 @@ * 2.0. */ -import { SPACES } from '../../common/lib/spaces'; import { - testCaseFailures, getTestScenarios, + testCaseFailures, } from '../../../saved_object_api_integration/common/lib/saved_object_test_utils'; +import type { FtrProviderContext } from '../../common/ftr_provider_context'; import { MULTI_NAMESPACE_SAVED_OBJECT_TEST_CASES as CASES } from '../../common/lib/saved_object_test_cases'; +import { SPACES } from '../../common/lib/spaces'; import type { UpdateObjectsSpacesTestCase } from '../../common/suites/update_objects_spaces'; import { updateObjectsSpacesTestSuiteFactory } from '../../common/suites/update_objects_spaces'; -import { FtrProviderContext } from '../../common/ftr_provider_context'; const { DEFAULT: { spaceId: DEFAULT_SPACE_ID }, diff --git a/x-pack/test/spaces_api_integration/spaces_only/telemetry/index.ts b/x-pack/test/spaces_api_integration/spaces_only/telemetry/index.ts index d2cd3fb8a4d6e..3b7f3e3232b99 100644 --- a/x-pack/test/spaces_api_integration/spaces_only/telemetry/index.ts +++ b/x-pack/test/spaces_api_integration/spaces_only/telemetry/index.ts @@ -5,7 +5,7 @@ * 2.0. */ -import { FtrProviderContext } from '../../common/ftr_provider_context'; +import type { FtrProviderContext } from '../../common/ftr_provider_context'; // eslint-disable-next-line import/no-default-export export default function spacesOnlyTestSuite({ loadTestFile }: FtrProviderContext) { diff --git a/x-pack/test/spaces_api_integration/spaces_only/telemetry/telemetry.ts b/x-pack/test/spaces_api_integration/spaces_only/telemetry/telemetry.ts index 98b3c88354593..a2b73f597414a 100644 --- a/x-pack/test/spaces_api_integration/spaces_only/telemetry/telemetry.ts +++ b/x-pack/test/spaces_api_integration/spaces_only/telemetry/telemetry.ts @@ -6,7 +6,8 @@ */ import expect from '@kbn/expect'; -import { FtrProviderContext } from '../../common/ftr_provider_context'; + +import type { 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_serverless/api_integration/test_suites/common/platform_security/authentication.ts b/x-pack/test_serverless/api_integration/test_suites/common/platform_security/authentication.ts index 7f31db43a3f00..644f0c5b852a7 100644 --- a/x-pack/test_serverless/api_integration/test_suites/common/platform_security/authentication.ts +++ b/x-pack/test_serverless/api_integration/test_suites/common/platform_security/authentication.ts @@ -6,51 +6,58 @@ */ import expect from 'expect'; +import { SupertestWithRoleScopeType } from '@kbn/test-suites-xpack/api_integration/deployment_agnostic/services'; import { FtrProviderContext } from '../../../ftr_provider_context'; -import { RoleCredentials } from '../../../../shared/services'; export default function ({ getService }: FtrProviderContext) { - const supertest = getService('supertest'); - const config = getService('config'); - + const roleScopedSupertest = getService('roleScopedSupertest'); const svlCommonApi = getService('svlCommonApi'); - const svlUserManager = getService('svlUserManager'); - const supertestWithoutAuth = getService('supertestWithoutAuth'); - let roleAuthc: RoleCredentials; + let supertestAdminWithApiKey: SupertestWithRoleScopeType; + let supertestViewerWithApiKey: SupertestWithRoleScopeType; + let supertestViewerWithCookieCredentials: SupertestWithRoleScopeType; + describe('security/authentication', function () { before(async () => { - roleAuthc = await svlUserManager.createM2mApiKeyWithRoleScope('admin'); + supertestAdminWithApiKey = await roleScopedSupertest.getSupertestWithRoleScope('admin'); + supertestViewerWithApiKey = await roleScopedSupertest.getSupertestWithRoleScope('viewer'); + supertestViewerWithCookieCredentials = await roleScopedSupertest.getSupertestWithRoleScope( + 'viewer', + { + useCookieHeader: true, + withCommonHeaders: true, + } + ); }); after(async () => { - await svlUserManager.invalidateM2mApiKeyWithRoleScope(roleAuthc); + await supertestAdminWithApiKey.destroy(); + await supertestViewerWithApiKey.destroy(); + await supertestViewerWithCookieCredentials.destroy(); }); describe('route access', () => { describe('disabled', () => { // ToDo: uncomment when we disable login // it('login', async () => { - // const { body, status } = await supertestWithoutAuth - // .post('/internal/security/login') - // .set(svlCommonApi.getInternalRequestHeader()).set(roleAuthc.apiKeyHeader) + // const { body, status } = await supertestAdminWithApiKey + // .post('/internal/security/login'); // svlCommonApi.assertApiNotFound(body, status); // }); it('logout (deprecated)', async () => { - const { body, status } = await supertestWithoutAuth + const { body, status } = await supertestAdminWithApiKey .get('/api/security/v1/logout') - .set(svlCommonApi.getInternalRequestHeader()) - .set(roleAuthc.apiKeyHeader); + .set(svlCommonApi.getInternalRequestHeader()); svlCommonApi.assertApiNotFound(body, status); }); it('get current user (deprecated)', async () => { - const { body, status } = await supertest + const { body, status } = await supertestAdminWithApiKey .get('/internal/security/v1/me') .set(svlCommonApi.getInternalRequestHeader()); svlCommonApi.assertApiNotFound(body, status); }); it('acknowledge access agreement', async () => { - const { body, status } = await supertest + const { body, status } = await supertestAdminWithApiKey .post('/internal/security/access_agreement/acknowledge') .set(svlCommonApi.getInternalRequestHeader()); svlCommonApi.assertApiNotFound(body, status); @@ -58,56 +65,56 @@ export default function ({ getService }: FtrProviderContext) { describe('OIDC', () => { it('OIDC implicit', async () => { - const { body, status } = await supertest + const { body, status } = await supertestAdminWithApiKey .get('/api/security/oidc/implicit') .set(svlCommonApi.getInternalRequestHeader()); svlCommonApi.assertApiNotFound(body, status); }); it('OIDC implicit (deprecated)', async () => { - const { body, status } = await supertest + const { body, status } = await supertestAdminWithApiKey .get('/api/security/v1/oidc/implicit') .set(svlCommonApi.getInternalRequestHeader()); svlCommonApi.assertApiNotFound(body, status); }); it('OIDC implicit.js', async () => { - const { body, status } = await supertest + const { body, status } = await supertestAdminWithApiKey .get('/internal/security/oidc/implicit.js') .set(svlCommonApi.getInternalRequestHeader()); svlCommonApi.assertApiNotFound(body, status); }); it('OIDC callback', async () => { - const { body, status } = await supertest + const { body, status } = await supertestAdminWithApiKey .get('/api/security/oidc/callback') .set(svlCommonApi.getInternalRequestHeader()); svlCommonApi.assertApiNotFound(body, status); }); it('OIDC callback (deprecated)', async () => { - const { body, status } = await supertest + const { body, status } = await supertestAdminWithApiKey .get('/api/security/v1/oidc') .set(svlCommonApi.getInternalRequestHeader()); svlCommonApi.assertApiNotFound(body, status); }); it('OIDC login', async () => { - const { body, status } = await supertest + const { body, status } = await supertestAdminWithApiKey .post('/api/security/oidc/initiate_login') .set(svlCommonApi.getInternalRequestHeader()); svlCommonApi.assertApiNotFound(body, status); }); it('OIDC login (deprecated)', async () => { - const { body, status } = await supertest + const { body, status } = await supertestAdminWithApiKey .post('/api/security/v1/oidc') .set(svlCommonApi.getInternalRequestHeader()); svlCommonApi.assertApiNotFound(body, status); }); it('OIDC 3rd party login', async () => { - const { body, status } = await supertest + const { body, status } = await supertestAdminWithApiKey .get('/api/security/oidc/initiate_login') .set(svlCommonApi.getInternalRequestHeader()); svlCommonApi.assertApiNotFound(body, status); @@ -115,7 +122,7 @@ export default function ({ getService }: FtrProviderContext) { }); it('SAML callback (deprecated)', async () => { - const { body, status } = await supertest + const { body, status } = await supertestAdminWithApiKey .post('/api/security/v1/saml') .set(svlCommonApi.getInternalRequestHeader()); svlCommonApi.assertApiNotFound(body, status); @@ -127,9 +134,9 @@ export default function ({ getService }: FtrProviderContext) { let body: any; let status: number; - ({ body, status } = await supertest - .get('/internal/security/me') - .set(svlCommonApi.getCommonRequestHeader())); + ({ body, status } = await supertestViewerWithCookieCredentials.get( + '/internal/security/me' + )); // expect a rejection because we're not using the internal header expect(body).toEqual({ statusCode: 400, @@ -140,24 +147,22 @@ export default function ({ getService }: FtrProviderContext) { }); expect(status).toBe(400); - ({ body, status } = await supertest + ({ body, status } = await supertestViewerWithCookieCredentials .get('/internal/security/me') .set(svlCommonApi.getInternalRequestHeader())); // expect success because we're using the internal header - expect(body).toEqual({ - authentication_provider: { name: '__http__', type: 'http' }, - authentication_realm: { name: 'file1', type: 'file' }, - authentication_type: 'realm', - elastic_cloud_user: false, - email: null, - enabled: true, - full_name: null, - lookup_realm: { name: 'file1', type: 'file' }, - metadata: {}, - operator: true, - roles: ['superuser'], - username: config.get('servers.kibana.username'), - }); + expect(body).toEqual( + expect.objectContaining({ + authentication_provider: { name: 'cloud-saml-kibana', type: 'saml' }, + authentication_type: 'token', + authentication_realm: { + name: 'cloud-saml-kibana', + type: 'saml', + }, + enabled: true, + roles: [expect.stringContaining('viewer')], + }) + ); expect(status).toBe(200); }); @@ -166,9 +171,9 @@ export default function ({ getService }: FtrProviderContext) { let body: any; let status: number; - ({ body, status } = await supertest - .post('/internal/security/login') - .set(svlCommonApi.getCommonRequestHeader())); + ({ body, status } = await supertestViewerWithCookieCredentials.post( + '/internal/security/login' + )); // expect a rejection because we're not using the internal header expect(body).toEqual({ statusCode: 400, @@ -179,7 +184,7 @@ export default function ({ getService }: FtrProviderContext) { }); expect(status).toBe(400); - ({ body, status } = await supertest + ({ body, status } = await supertestViewerWithCookieCredentials .post('/internal/security/login') .set(svlCommonApi.getInternalRequestHeader())); expect(status).not.toBe(404); @@ -188,12 +193,12 @@ export default function ({ getService }: FtrProviderContext) { describe('public', () => { it('logout', async () => { - const { status } = await supertest.get('/api/security/logout'); + const { status } = await supertestViewerWithApiKey.get('/api/security/logout'); expect(status).toBe(302); }); it('SAML callback', async () => { - const { body, status } = await supertest + const { body, status } = await supertestViewerWithApiKey .post('/api/security/saml/callback') .set(svlCommonApi.getCommonRequestHeader()) .send({ diff --git a/x-pack/test_serverless/api_integration/test_suites/common/platform_security/authorization.ts b/x-pack/test_serverless/api_integration/test_suites/common/platform_security/authorization.ts index bc01b14848eff..bd706132d4874 100644 --- a/x-pack/test_serverless/api_integration/test_suites/common/platform_security/authorization.ts +++ b/x-pack/test_serverless/api_integration/test_suites/common/platform_security/authorization.ts @@ -75,7 +75,7 @@ export default function ({ getService }: FtrProviderContext) { it('get role', async () => { const { body, status } = await supertestAdminWithApiKey.get( - '/api/security/role/superuser' + '/api/security/role/someRole' // mame of the role doesn't matter, we're checking the endpoint doesn't exist ); svlCommonApi.assertApiNotFound(body, status); }); @@ -87,7 +87,7 @@ export default function ({ getService }: FtrProviderContext) { it('delete role', async () => { const { body, status } = await supertestAdminWithApiKey.delete( - '/api/security/role/superuser' + '/api/security/role/someRole' // mame of the role doesn't matter, we're checking the endpoint doesn't exist ); svlCommonApi.assertApiNotFound(body, status); }); diff --git a/x-pack/test_serverless/api_integration/test_suites/security/cloud_security_posture/serverless_metering/cloud_security_metering.ts b/x-pack/test_serverless/api_integration/test_suites/security/cloud_security_posture/serverless_metering/cloud_security_metering.ts index b3db98c829afd..f3d613a41d590 100644 --- a/x-pack/test_serverless/api_integration/test_suites/security/cloud_security_posture/serverless_metering/cloud_security_metering.ts +++ b/x-pack/test_serverless/api_integration/test_suites/security/cloud_security_posture/serverless_metering/cloud_security_metering.ts @@ -10,11 +10,10 @@ import { CDR_LATEST_NATIVE_VULNERABILITIES_INDEX_PATTERN } from '@kbn/cloud-secu import { LATEST_FINDINGS_INDEX_DEFAULT_NS } from '@kbn/cloud-security-posture-plugin/common/constants'; import * as http from 'http'; import { - deleteIndex, createPackagePolicy, createCloudDefendPackagePolicy, - bulkIndex, } from '@kbn/test-suites-xpack/api_integration/apis/cloud_security_posture/helper'; +import { EsIndexDataProvider } from '@kbn/test-suites-xpack/cloud_security_posture_api/utils'; import { RoleCredentials } from '../../../../../shared/services'; import { getMockFindings, getMockDefendForContainersHeartbeats } from './mock_data'; import type { FtrProviderContext } from '../../../../ftr_provider_context'; @@ -32,6 +31,12 @@ export default function (providerContext: FtrProviderContext) { const svlCommonApi = getService('svlCommonApi'); const svlUserManager = getService('svlUserManager'); const supertestWithoutAuth = getService('supertestWithoutAuth'); + const findingsIndex = new EsIndexDataProvider(es, LATEST_FINDINGS_INDEX_DEFAULT_NS); + const cloudDefinedIndex = new EsIndexDataProvider(es, CLOUD_DEFEND_HEARTBEAT_INDEX_DEFAULT_NS); + const vulnerabilitiesIndex = new EsIndexDataProvider( + es, + CDR_LATEST_NATIVE_VULNERABILITIES_INDEX_PATTERN + ); /* This test aims to intercept the usage API request sent by the metering background task manager. @@ -67,25 +72,17 @@ export default function (providerContext: FtrProviderContext) { agentPolicyId = agentPolicyResponse.item.id; - await deleteIndex(es, [ - LATEST_FINDINGS_INDEX_DEFAULT_NS, - CDR_LATEST_NATIVE_VULNERABILITIES_INDEX_PATTERN, - CLOUD_DEFEND_HEARTBEAT_INDEX_DEFAULT_NS, - ]); + await findingsIndex.deleteAll(); + await vulnerabilitiesIndex.deleteAll(); + await cloudDefinedIndex.deleteAll(); }); afterEach(async () => { - await deleteIndex(es, [ - LATEST_FINDINGS_INDEX_DEFAULT_NS, - CDR_LATEST_NATIVE_VULNERABILITIES_INDEX_PATTERN, - ]); await kibanaServer.savedObjects.cleanStandardList(); await esArchiver.unload('x-pack/test/functional/es_archives/fleet/empty_fleet_server'); - await deleteIndex(es, [ - LATEST_FINDINGS_INDEX_DEFAULT_NS, - CDR_LATEST_NATIVE_VULNERABILITIES_INDEX_PATTERN, - CLOUD_DEFEND_HEARTBEAT_INDEX_DEFAULT_NS, - ]); + await findingsIndex.deleteAll(); + await vulnerabilitiesIndex.deleteAll(); + await cloudDefinedIndex.deleteAll(); }); after(async () => { await svlUserManager.invalidateM2mApiKeyWithRoleScope(roleAuthc); @@ -116,11 +113,7 @@ export default function (providerContext: FtrProviderContext) { numberOfFindings: 10, }); - await bulkIndex( - es, - [...billableFindings, ...notBillableFindings], - LATEST_FINDINGS_INDEX_DEFAULT_NS - ); + await findingsIndex.addBulk([...billableFindings, ...notBillableFindings]); let interceptedRequestBody: UsageRecord[] = []; await retry.try(async () => { @@ -160,11 +153,7 @@ export default function (providerContext: FtrProviderContext) { numberOfFindings: 11, }); - await bulkIndex( - es, - [...billableFindings, ...notBillableFindings], - LATEST_FINDINGS_INDEX_DEFAULT_NS - ); + await findingsIndex.addBulk([...billableFindings, ...notBillableFindings]); let interceptedRequestBody: UsageRecord[] = []; @@ -199,7 +188,7 @@ export default function (providerContext: FtrProviderContext) { numberOfFindings: 2, }); - await bulkIndex(es, billableFindings, CDR_LATEST_NATIVE_VULNERABILITIES_INDEX_PATTERN); + await vulnerabilitiesIndex.addBulk(billableFindings); let interceptedRequestBody: UsageRecord[] = []; @@ -233,11 +222,11 @@ export default function (providerContext: FtrProviderContext) { isBlockActionEnables: false, numberOfHearbeats: 2, }); - await bulkIndex( - es, - [...blockActionEnabledHeartbeats, ...blockActionDisabledHeartbeats], - CLOUD_DEFEND_HEARTBEAT_INDEX_DEFAULT_NS - ); + + await cloudDefinedIndex.addBulk([ + ...blockActionEnabledHeartbeats, + ...blockActionDisabledHeartbeats, + ]); let interceptedRequestBody: UsageRecord[] = []; @@ -315,22 +304,17 @@ export default function (providerContext: FtrProviderContext) { }); await Promise.all([ - bulkIndex( - es, - [ - ...billableFindingsCSPM, - ...notBillableFindingsCSPM, - ...billableFindingsKSPM, - ...notBillableFindingsKSPM, - ], - LATEST_FINDINGS_INDEX_DEFAULT_NS - ), - bulkIndex(es, [...billableFindingsCNVM], CDR_LATEST_NATIVE_VULNERABILITIES_INDEX_PATTERN), - bulkIndex( - es, - [...blockActionEnabledHeartbeats, ...blockActionDisabledHeartbeats], - CLOUD_DEFEND_HEARTBEAT_INDEX_DEFAULT_NS - ), + findingsIndex.addBulk([ + ...billableFindingsCSPM, + ...notBillableFindingsCSPM, + ...billableFindingsKSPM, + ...notBillableFindingsKSPM, + ]), + vulnerabilitiesIndex.addBulk([...billableFindingsCNVM]), + cloudDefinedIndex.addBulk([ + ...blockActionEnabledHeartbeats, + ...blockActionDisabledHeartbeats, + ]), ]); // Intercept and verify usage API request diff --git a/x-pack/test_serverless/api_integration/test_suites/security/cloud_security_posture/serverless_metering/mock_data.ts b/x-pack/test_serverless/api_integration/test_suites/security/cloud_security_posture/serverless_metering/mock_data.ts index 5e5844eaaf3b5..1991b53b85b35 100644 --- a/x-pack/test_serverless/api_integration/test_suites/security/cloud_security_posture/serverless_metering/mock_data.ts +++ b/x-pack/test_serverless/api_integration/test_suites/security/cloud_security_posture/serverless_metering/mock_data.ts @@ -82,6 +82,8 @@ const mockFiniding = (postureType: string, isBillableAsset?: boolean) => { }, }; } + + throw new Error('Invalid posture type'); }; export const getMockDefendForContainersHeartbeats = ({ diff --git a/x-pack/test_serverless/api_integration/test_suites/security/cloud_security_posture/status/status_indexed.ts b/x-pack/test_serverless/api_integration/test_suites/security/cloud_security_posture/status/status_indexed.ts index a9da3a42cdfc8..b53163796a6ee 100644 --- a/x-pack/test_serverless/api_integration/test_suites/security/cloud_security_posture/status/status_indexed.ts +++ b/x-pack/test_serverless/api_integration/test_suites/security/cloud_security_posture/status/status_indexed.ts @@ -8,16 +8,9 @@ import expect from '@kbn/expect'; import { ELASTIC_HTTP_VERSION_HEADER } from '@kbn/core-http-common'; import type { CspSetupStatus } from '@kbn/cloud-security-posture-common'; import { CDR_LATEST_NATIVE_VULNERABILITIES_INDEX_PATTERN } from '@kbn/cloud-security-posture-common'; -import { - FINDINGS_INDEX_DEFAULT_NS, - LATEST_FINDINGS_INDEX_DEFAULT_NS, - VULNERABILITIES_INDEX_DEFAULT_NS, -} from '@kbn/cloud-security-posture-plugin/common/constants'; -import { - deleteIndex, - addIndex, - createPackagePolicy, -} from '@kbn/test-suites-xpack/api_integration/apis/cloud_security_posture/helper'; +import { LATEST_FINDINGS_INDEX_DEFAULT_NS } from '@kbn/cloud-security-posture-plugin/common/constants'; +import { createPackagePolicy } from '@kbn/test-suites-xpack/api_integration/apis/cloud_security_posture/helper'; +import { EsIndexDataProvider } from '@kbn/test-suites-xpack/cloud_security_posture_api/utils'; import { findingsMockData, vulnerabilityMockData, @@ -25,13 +18,6 @@ import { import { FtrProviderContext } from '../../../../ftr_provider_context'; import { RoleCredentials } from '../../../../../shared/services'; -const INDEX_ARRAY = [ - FINDINGS_INDEX_DEFAULT_NS, - LATEST_FINDINGS_INDEX_DEFAULT_NS, - CDR_LATEST_NATIVE_VULNERABILITIES_INDEX_PATTERN, - VULNERABILITIES_INDEX_DEFAULT_NS, -]; - export default function (providerContext: FtrProviderContext) { const { getService } = providerContext; const es = getService('es'); @@ -40,6 +26,11 @@ export default function (providerContext: FtrProviderContext) { const supertestWithoutAuth = getService('supertestWithoutAuth'); const svlCommonApi = getService('svlCommonApi'); const svlUserManager = getService('svlUserManager'); + const latestFindingsIndex = new EsIndexDataProvider(es, LATEST_FINDINGS_INDEX_DEFAULT_NS); + const latestVulnerabilitiesIndex = new EsIndexDataProvider( + es, + CDR_LATEST_NATIVE_VULNERABILITIES_INDEX_PATTERN + ); describe('GET /internal/cloud_security_posture/status', function () { // security_exception: action [indices:admin/create] is unauthorized for user [elastic] with effective roles [superuser] on restricted indices [.fleet-actions-7], this action is granted by the index privileges [create_index,manage,all] @@ -74,13 +65,13 @@ export default function (providerContext: FtrProviderContext) { agentPolicyId = agentPolicyResponse.item.id; - await deleteIndex(es, INDEX_ARRAY); - await addIndex(es, findingsMockData, LATEST_FINDINGS_INDEX_DEFAULT_NS); - await addIndex(es, vulnerabilityMockData, CDR_LATEST_NATIVE_VULNERABILITIES_INDEX_PATTERN); + await latestFindingsIndex.deleteAll(); + await latestVulnerabilitiesIndex.deleteAll(); }); afterEach(async () => { - await deleteIndex(es, INDEX_ARRAY); + await latestFindingsIndex.deleteAll(); + await latestVulnerabilitiesIndex.deleteAll(); await kibanaServer.savedObjects.cleanStandardList(); await esArchiver.unload('x-pack/test/functional/es_archives/fleet/empty_fleet_server'); }); @@ -98,6 +89,8 @@ export default function (providerContext: FtrProviderContext) { internalRequestHeader ); + await latestFindingsIndex.addBulk(findingsMockData); + const { body: res }: { body: CspSetupStatus } = await supertestWithoutAuth .get(`/internal/cloud_security_posture/status`) .set(ELASTIC_HTTP_VERSION_HEADER, '1') @@ -124,6 +117,8 @@ export default function (providerContext: FtrProviderContext) { internalRequestHeader ); + await latestFindingsIndex.addBulk(findingsMockData); + const { body: res }: { body: CspSetupStatus } = await supertestWithoutAuth .get(`/internal/cloud_security_posture/status`) .set(ELASTIC_HTTP_VERSION_HEADER, '1') @@ -150,6 +145,8 @@ export default function (providerContext: FtrProviderContext) { internalRequestHeader ); + await latestVulnerabilitiesIndex.addBulk(vulnerabilityMockData); + const { body: res }: { body: CspSetupStatus } = await supertestWithoutAuth .get(`/internal/cloud_security_posture/status`) .set(ELASTIC_HTTP_VERSION_HEADER, '1') diff --git a/x-pack/test_serverless/api_integration/test_suites/security/cloud_security_posture/status/status_indexing.ts b/x-pack/test_serverless/api_integration/test_suites/security/cloud_security_posture/status/status_indexing.ts index ec6a5835e6aa3..e531f2a5cc14e 100644 --- a/x-pack/test_serverless/api_integration/test_suites/security/cloud_security_posture/status/status_indexing.ts +++ b/x-pack/test_serverless/api_integration/test_suites/security/cloud_security_posture/status/status_indexing.ts @@ -7,31 +7,19 @@ import expect from '@kbn/expect'; import { ELASTIC_HTTP_VERSION_HEADER } from '@kbn/core-http-common'; import type { CspSetupStatus } from '@kbn/cloud-security-posture-common'; -import { CDR_LATEST_NATIVE_VULNERABILITIES_INDEX_PATTERN } from '@kbn/cloud-security-posture-common'; import { FINDINGS_INDEX_DEFAULT_NS, - LATEST_FINDINGS_INDEX_DEFAULT_NS, VULNERABILITIES_INDEX_DEFAULT_NS, } from '@kbn/cloud-security-posture-plugin/common/constants'; -import { - deleteIndex, - addIndex, - createPackagePolicy, -} from '@kbn/test-suites-xpack/api_integration/apis/cloud_security_posture/helper'; +import { createPackagePolicy } from '@kbn/test-suites-xpack/api_integration/apis/cloud_security_posture/helper'; import { findingsMockData, vulnerabilityMockData, } from '@kbn/test-suites-xpack/api_integration/apis/cloud_security_posture/mock_data'; +import { EsIndexDataProvider } from '@kbn/test-suites-xpack/cloud_security_posture_api/utils'; import { FtrProviderContext } from '../../../../ftr_provider_context'; import { RoleCredentials } from '../../../../../shared/services'; -const INDEX_ARRAY = [ - FINDINGS_INDEX_DEFAULT_NS, - LATEST_FINDINGS_INDEX_DEFAULT_NS, - CDR_LATEST_NATIVE_VULNERABILITIES_INDEX_PATTERN, - VULNERABILITIES_INDEX_DEFAULT_NS, -]; - export default function (providerContext: FtrProviderContext) { const { getService } = providerContext; const es = getService('es'); @@ -40,6 +28,8 @@ export default function (providerContext: FtrProviderContext) { const supertestWithoutAuth = getService('supertestWithoutAuth'); const svlCommonApi = getService('svlCommonApi'); const svlUserManager = getService('svlUserManager'); + const findingsIndex = new EsIndexDataProvider(es, FINDINGS_INDEX_DEFAULT_NS); + const vulnerabilitiesIndex = new EsIndexDataProvider(es, VULNERABILITIES_INDEX_DEFAULT_NS); describe('GET /internal/cloud_security_posture/status', function () { // security_exception: action [indices:admin/create] is unauthorized for user [elastic] with effective roles [superuser] on restricted indices [.fleet-actions-7], this action is granted by the index privileges [create_index,manage,all] @@ -73,13 +63,13 @@ export default function (providerContext: FtrProviderContext) { }); agentPolicyId = agentPolicyResponse.item.id; - await deleteIndex(es, INDEX_ARRAY); - await addIndex(es, findingsMockData, FINDINGS_INDEX_DEFAULT_NS); - await addIndex(es, vulnerabilityMockData, VULNERABILITIES_INDEX_DEFAULT_NS); + await findingsIndex.deleteAll(); + await vulnerabilitiesIndex.deleteAll(); }); afterEach(async () => { - await deleteIndex(es, INDEX_ARRAY); + await findingsIndex.deleteAll(); + await vulnerabilitiesIndex.deleteAll(); await kibanaServer.savedObjects.cleanStandardList(); await esArchiver.unload('x-pack/test/functional/es_archives/fleet/empty_fleet_server'); }); @@ -97,6 +87,8 @@ export default function (providerContext: FtrProviderContext) { internalRequestHeader ); + await findingsIndex.addBulk(findingsMockData); + const { body: res }: { body: CspSetupStatus } = await supertestWithoutAuth .get(`/internal/cloud_security_posture/status`) .set(ELASTIC_HTTP_VERSION_HEADER, '1') @@ -123,6 +115,8 @@ export default function (providerContext: FtrProviderContext) { internalRequestHeader ); + await findingsIndex.addBulk(findingsMockData); + const { body: res }: { body: CspSetupStatus } = await supertestWithoutAuth .get(`/internal/cloud_security_posture/status`) .set(ELASTIC_HTTP_VERSION_HEADER, '1') @@ -149,6 +143,8 @@ export default function (providerContext: FtrProviderContext) { internalRequestHeader ); + await vulnerabilitiesIndex.addBulk(vulnerabilityMockData); + const { body: res }: { body: CspSetupStatus } = await supertestWithoutAuth .get(`/internal/cloud_security_posture/status`) .set(ELASTIC_HTTP_VERSION_HEADER, '1') diff --git a/x-pack/test_serverless/api_integration/test_suites/security/cloud_security_posture/telemetry.ts b/x-pack/test_serverless/api_integration/test_suites/security/cloud_security_posture/telemetry.ts index 62cf85b47d997..15700419a7e96 100644 --- a/x-pack/test_serverless/api_integration/test_suites/security/cloud_security_posture/telemetry.ts +++ b/x-pack/test_serverless/api_integration/test_suites/security/cloud_security_posture/telemetry.ts @@ -7,11 +7,12 @@ import expect from '@kbn/expect'; import { ELASTIC_HTTP_VERSION_HEADER } from '@kbn/core-http-common'; -import { - data as telemetryMockData, - MockTelemetryFindings, -} from '@kbn/test-suites-xpack/cloud_security_posture_api/telemetry/data'; +import { data as telemetryMockData } from '@kbn/test-suites-xpack/cloud_security_posture_api/telemetry/data'; import { createPackagePolicy } from '@kbn/test-suites-xpack/api_integration/apis/cloud_security_posture/helper'; +import { + waitForPluginInitialized, + EsIndexDataProvider, +} from '@kbn/test-suites-xpack/cloud_security_posture_api/utils'; import { SupertestWithRoleScopeType } from '@kbn/test-suites-xpack/api_integration/deployment_agnostic/services'; import type { FtrProviderContext } from '../../../ftr_provider_context'; import { RoleCredentials } from '../../../../shared/services'; @@ -21,7 +22,7 @@ const FINDINGS_INDEX = 'logs-cloud_security_posture.findings_latest-default'; export default function ({ getService }: FtrProviderContext) { const retry = getService('retry'); const es = getService('es'); - const log = getService('log'); + const logger = getService('log'); const esArchiver = getService('esArchiver'); const kibanaServer = getService('kibanaServer'); const supertestWithoutAuth = getService('supertestWithoutAuth'); @@ -33,24 +34,7 @@ export default function ({ getService }: FtrProviderContext) { let roleAuthc: RoleCredentials; let internalRequestHeader: { 'x-elastic-internal-origin': string; 'kbn-xsrf': string }; - const index = { - remove: () => - es.deleteByQuery({ - index: FINDINGS_INDEX, - query: { match_all: {} }, - refresh: true, - }), - - add: async (mockTelemetryFindings: MockTelemetryFindings[]) => { - const operations = mockTelemetryFindings.flatMap((doc) => [ - { index: { _index: FINDINGS_INDEX } }, - doc, - ]); - - const response = await es.bulk({ refresh: 'wait_for', index: FINDINGS_INDEX, operations }); - expect(response.errors).to.eql(false); - }, - }; + const findingsIndex = new EsIndexDataProvider(es, FINDINGS_INDEX); describe('Verify cloud_security_posture telemetry payloads', function () { // security_exception: action [indices:admin/create] is unauthorized for user [elastic] with effective roles [superuser] on restricted indices [.fleet-actions-7], this action is granted by the index privileges [create_index,manage,all] @@ -95,22 +79,11 @@ export default function ({ getService }: FtrProviderContext) { internalRequestHeader ); - log.debug('Check CSP plugin is initialized'); - await retry.try(async () => { - const supertestAdminWithHttpHeaderV1 = await roleScopedSupertest.getSupertestWithRoleScope( - 'admin', - { - useCookieHeader: true, - withInternalHeaders: true, - withCustomHeaders: { [ELASTIC_HTTP_VERSION_HEADER]: '1' }, - } - ); - const response = await supertestAdminWithHttpHeaderV1 - .get('/internal/cloud_security_posture/status?check=init') - .expect(200); - expect(response.body).to.eql({ isPluginInitialized: true }); - log.debug('CSP plugin is initialized'); + const supertestAdmin = await roleScopedSupertest.getSupertestWithRoleScope('admin', { + useCookieHeader: true, + withInternalHeaders: true, }); + await waitForPluginInitialized({ logger, retry, supertest: supertestAdmin }); }); after(async () => { @@ -120,11 +93,11 @@ export default function ({ getService }: FtrProviderContext) { }); afterEach(async () => { - await index.remove(); + await findingsIndex.deleteAll(); }); it('includes only KSPM findings', async () => { - await index.add(telemetryMockData.kspmFindings); + await findingsIndex.addBulk(telemetryMockData.kspmFindings); const { body: [{ stats: apiResponse }], @@ -175,7 +148,7 @@ export default function ({ getService }: FtrProviderContext) { }); it('includes only CSPM findings', async () => { - await index.add(telemetryMockData.cspmFindings); + await findingsIndex.addBulk(telemetryMockData.cspmFindings); const { body: [{ stats: apiResponse }], @@ -218,8 +191,10 @@ export default function ({ getService }: FtrProviderContext) { }); it('includes CSPM and KSPM findings', async () => { - await index.add(telemetryMockData.kspmFindings); - await index.add(telemetryMockData.cspmFindings); + await findingsIndex.addBulk([ + ...telemetryMockData.kspmFindings, + ...telemetryMockData.cspmFindings, + ]); const { body: [{ stats: apiResponse }], @@ -294,7 +269,7 @@ export default function ({ getService }: FtrProviderContext) { }); it(`'includes only KSPM findings without posture_type'`, async () => { - await index.add(telemetryMockData.kspmFindingsNoPostureType); + await findingsIndex.addBulk(telemetryMockData.kspmFindingsNoPostureType); const { body: [{ stats: apiResponse }], @@ -346,8 +321,10 @@ export default function ({ getService }: FtrProviderContext) { }); it('includes KSPM findings without posture_type and CSPM findings as well', async () => { - await index.add(telemetryMockData.kspmFindingsNoPostureType); - await index.add(telemetryMockData.cspmFindings); + await findingsIndex.addBulk([ + ...telemetryMockData.kspmFindingsNoPostureType, + ...telemetryMockData.cspmFindings, + ]); const { body: [{ stats: apiResponse }], diff --git a/x-pack/test_serverless/api_integration/test_suites/security/config.ts b/x-pack/test_serverless/api_integration/test_suites/security/config.ts index d40cde3c25837..0b24438b81591 100644 --- a/x-pack/test_serverless/api_integration/test_suites/security/config.ts +++ b/x-pack/test_serverless/api_integration/test_suites/security/config.ts @@ -24,6 +24,6 @@ export default createTestConfig({ // useful for testing (also enabled in MKI QA) '--coreApp.allowDynamicConfigOverrides=true', `--xpack.securitySolutionServerless.cloudSecurityUsageReportingTaskInterval=5s`, - `--xpack.securitySolutionServerless.usageApi.url=http://localhost:8081/api/v1/usage`, + `--xpack.securitySolutionServerless.usageApi.url=http://localhost:8081`, ], }); diff --git a/x-pack/test_serverless/functional/test_suites/common/dev_tools/search_profiler.ts b/x-pack/test_serverless/functional/test_suites/common/dev_tools/search_profiler.ts index 6a908ce4e0fe8..979943ffa602c 100644 --- a/x-pack/test_serverless/functional/test_suites/common/dev_tools/search_profiler.ts +++ b/x-pack/test_serverless/functional/test_suites/common/dev_tools/search_profiler.ts @@ -8,7 +8,7 @@ import expect from '@kbn/expect'; import { FtrProviderContext } from '../../../ftr_provider_context'; -const testIndex = 'test-index'; +const indexName = 'my_index'; const testQuery = { query: { match_all: {}, @@ -53,10 +53,6 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) { }, }; - // Since we're not actually running the query in the test, - // this index name is just an input placeholder and does not exist - const indexName = 'my_index'; - await PageObjects.common.navigateToUrl( 'searchProfiler', PageObjects.searchProfiler.getUrlWithIndexAndQuery({ indexName, query }), @@ -77,21 +73,21 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) { describe('With a test index', () => { before(async () => { - await es.indices.create({ index: testIndex }); + await es.indices.create({ index: indexName }); }); after(async () => { - await es.indices.delete({ index: testIndex }); + await es.indices.delete({ index: indexName }); }); it('profiles a simple query', async () => { - await PageObjects.searchProfiler.setIndexName(testIndex); + await PageObjects.searchProfiler.setIndexName(indexName); await PageObjects.searchProfiler.setQuery(testQuery); await PageObjects.searchProfiler.clickProfileButton(); const content = await PageObjects.searchProfiler.getProfileContent(); - expect(content).to.contain(testIndex); + expect(content).to.contain(indexName); }); }); }); diff --git a/x-pack/test_serverless/functional/test_suites/observability/dataset_quality/degraded_field_flyout.ts b/x-pack/test_serverless/functional/test_suites/observability/dataset_quality/degraded_field_flyout.ts index 263dc8652ad75..f5f0b1c76ee8e 100644 --- a/x-pack/test_serverless/functional/test_suites/observability/dataset_quality/degraded_field_flyout.ts +++ b/x-pack/test_serverless/functional/test_suites/observability/dataset_quality/degraded_field_flyout.ts @@ -38,7 +38,9 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { const serviceName = 'test_service'; const count = 5; - describe('Degraded fields flyout', () => { + describe('Degraded fields flyout', function () { + // see details: https://github.com/elastic/kibana/issues/195466 + this.tags(['failsOnMKI']); before(async () => { await synthtrace.index([ // Ingest basic logs diff --git a/x-pack/test_serverless/functional/test_suites/security/ftr/cloud_security_posture/agentless/cis_integration_aws.ts b/x-pack/test_serverless/functional/test_suites/security/ftr/cloud_security_posture/agentless/cis_integration_aws.ts index 90991304936ea..e669545d135f9 100644 --- a/x-pack/test_serverless/functional/test_suites/security/ftr/cloud_security_posture/agentless/cis_integration_aws.ts +++ b/x-pack/test_serverless/functional/test_suites/security/ftr/cloud_security_posture/agentless/cis_integration_aws.ts @@ -27,7 +27,6 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) { let cisIntegrationAws: typeof pageObjects.cisAddIntegration.cisAws; let testSubjectIds: typeof pageObjects.cisAddIntegration.testSubjectIds; let mockApiServer: http.Server; - const previousPackageVersion = '1.9.0'; before(async () => { mockApiServer = mockAgentlessApiService.listen(8089); @@ -66,20 +65,6 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) { (await cisIntegrationAws.showLaunchCloudFormationAgentlessButton()) !== undefined ).to.be(true); }); - - it(`should hide CIS_AWS Launch Cloud formation button when credentials selector is temporary keys and package version is less than ${previousPackageVersion}`, async () => { - await cisIntegration.navigateToAddIntegrationCspmWithVersionPage(previousPackageVersion); - - await cisIntegration.clickOptionButton(testSubjectIds.CIS_AWS_OPTION_TEST_ID); - await cisIntegration.clickOptionButton(testSubjectIds.AWS_SINGLE_ACCOUNT_TEST_ID); - await cisIntegration.selectSetupTechnology('agentless'); - - await cisIntegration.selectAwsCredentials('temporary'); - - await pageObjects.header.waitUntilLoadingHasFinished(); - - expect(await cisIntegrationAws.showLaunchCloudFormationAgentlessButton()).to.be(false); - }); }); describe('Serverless - Agentless CIS_AWS ORG Account Launch Cloud formation', () => { @@ -100,19 +85,6 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) { expect(await cisIntegrationAws.showLaunchCloudFormationAgentlessButton()).to.be(true); }); - - it(`should hide CIS_AWS Launch Cloud formation button when credentials selector is temporary keys and package version is less than ${previousPackageVersion}`, async () => { - await cisIntegration.navigateToAddIntegrationCspmWithVersionPage(previousPackageVersion); - - await cisIntegration.clickOptionButton(testSubjectIds.CIS_AWS_OPTION_TEST_ID); - await cisIntegration.selectSetupTechnology('agentless'); - - await cisIntegration.selectAwsCredentials('temporary'); - - await pageObjects.header.waitUntilLoadingHasFinished(); - - expect(await cisIntegrationAws.showLaunchCloudFormationAgentlessButton()).to.be(false); - }); }); // TODO: Migrate test after Serverless default agentless policy is deleted. diff --git a/x-pack/test_serverless/functional/test_suites/security/ftr/cloud_security_posture/agentless/cis_integration_gcp.ts b/x-pack/test_serverless/functional/test_suites/security/ftr/cloud_security_posture/agentless/cis_integration_gcp.ts index 85a45f67bf9cc..95f855697c5bd 100644 --- a/x-pack/test_serverless/functional/test_suites/security/ftr/cloud_security_posture/agentless/cis_integration_gcp.ts +++ b/x-pack/test_serverless/functional/test_suites/security/ftr/cloud_security_posture/agentless/cis_integration_gcp.ts @@ -14,7 +14,6 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) { const pageObjects = getPageObjects(['common', 'svlCommonPage', 'cisAddIntegration', 'header']); const supertest = getService('supertest'); - const previousPackageVersion = '1.9.0'; describe('Agentless CIS Integration Page', function () { // TODO: we need to check if the tests are running on MKI. There is a suspicion that installing csp package via Kibana server args is not working on MKI. @@ -60,18 +59,6 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) { expect(await cisIntegrationGcp.showLaunchCloudShellAgentlessButton()).to.be(true); }); - - it(`should hide CIS_GCP Launch Cloud Shell button when package version is less than ${CLOUD_CREDENTIALS_PACKAGE_VERSION}`, async () => { - await cisIntegration.navigateToAddIntegrationCspmWithVersionPage(previousPackageVersion); - - await cisIntegration.clickOptionButton(testSubjectIds.CIS_GCP_OPTION_TEST_ID); - await cisIntegration.clickOptionButton(testSubjectIds.GCP_SINGLE_ACCOUNT_TEST_ID); - await cisIntegration.selectSetupTechnology('agentless'); - - await pageObjects.header.waitUntilLoadingHasFinished(); - - expect(await cisIntegrationGcp.showLaunchCloudShellAgentlessButton()).to.be(false); - }); }); describe('Agentless CIS_GCP ORG Account Launch Cloud Shell', () => { @@ -87,17 +74,6 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) { expect(await cisIntegrationGcp.showLaunchCloudShellAgentlessButton()).to.be(true); }); - - it(`should hide CIS_GCP Launch Cloud shell button when package version is ${previousPackageVersion}`, async () => { - await cisIntegration.navigateToAddIntegrationCspmWithVersionPage(previousPackageVersion); - - await cisIntegration.clickOptionButton(testSubjectIds.CIS_GCP_OPTION_TEST_ID); - await cisIntegration.selectSetupTechnology('agentless'); - - await pageObjects.header.waitUntilLoadingHasFinished(); - - expect(await cisIntegrationGcp.showLaunchCloudShellAgentlessButton()).to.be(false); - }); }); describe.skip('Serverless - Agentless CIS_GCP edit flow', () => { diff --git a/yarn.lock b/yarn.lock index c3ec4698df7d2..abc5b5ee2874d 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1643,10 +1643,10 @@ dependencies: object-hash "^1.3.0" -"@elastic/charts@67.0.1": - version "67.0.1" - resolved "https://registry.yarnpkg.com/@elastic/charts/-/charts-67.0.1.tgz#17ba397a97f207f99b6f682f136dde0aef474b57" - integrity sha512-6H9DUxm1vwp/R78PAx/zkBKechXF0g1LQuflfpfxMFplwRRw7OTz9cMMRjvrqUp1bVhkp9yLb4CWao+HWaIofA== +"@elastic/charts@68.0.0": + version "68.0.0" + resolved "https://registry.yarnpkg.com/@elastic/charts/-/charts-68.0.0.tgz#a61cc39d6b139006946a134f372c6af5e09fbadb" + integrity sha512-qtlI5U9olt9Y5ZkVUwy9VT2uvU8WzmCTYLjCM+l3b1g4ek0WW1ehZxaJha2Esqe5+QPSBH9LrVzBTayQbbprUA== dependencies: "@popperjs/core" "^2.11.8" bezier-easing "^2.1.0" @@ -3295,10 +3295,6 @@ version "0.0.0" uid "" -"@kbn/ace@link:packages/kbn-ace": - version "0.0.0" - uid "" - "@kbn/actions-plugin@link:x-pack/plugins/actions": version "0.0.0" uid "" @@ -3319,6 +3315,10 @@ version "0.0.0" uid "" +"@kbn/ai-assistant@link:x-pack/packages/kbn-ai-assistant": + version "0.0.0" + uid "" + "@kbn/aiops-change-point-detection@link:x-pack/packages/ml/aiops_change_point_detection": version "0.0.0" uid "" @@ -5879,6 +5879,10 @@ version "0.0.0" uid "" +"@kbn/observability-logs-overview@link:x-pack/packages/observability/logs_overview": + version "0.0.0" + uid "" + "@kbn/observability-onboarding-e2e@link:x-pack/plugins/observability_solution/observability_onboarding/e2e": version "0.0.0" uid "" @@ -12105,6 +12109,14 @@ use-isomorphic-layout-effect "^1.1.2" use-sync-external-store "^1.0.0" +"@xstate5/react@npm:@xstate/react@^4.1.2": + version "4.1.2" + resolved "https://registry.yarnpkg.com/@xstate/react/-/react-4.1.2.tgz#4bfcdf2d9e9ef1eaea7388d1896649345e6679cd" + integrity sha512-orAidFrKCrU0ZwN5l/ABPlBfW2ziRDT2RrYoktRlZ0WRoLvA2E/uAC1JpZt43mCLtc8jrdwYCgJiqx1V8NvGTw== + dependencies: + use-isomorphic-layout-effect "^1.1.2" + use-sync-external-store "^1.2.0" + "@xtuc/ieee754@^1.2.0": version "1.2.0" resolved "https://registry.yarnpkg.com/@xtuc/ieee754/-/ieee754-1.2.0.tgz#eef014a3145ae477a1cbc00cd1e552336dceb790" @@ -13567,11 +13579,6 @@ brace-expansion@^2.0.1: dependencies: balanced-match "^1.0.0" -brace@0.11.1, brace@^0.11.1: - version "0.11.1" - resolved "https://registry.yarnpkg.com/brace/-/brace-0.11.1.tgz#4896fcc9d544eef45f4bb7660db320d3b379fe58" - integrity sha1-SJb8ydVE7vRfS7dmDbMg07N5/lg= - braces@^2.3.1: version "2.3.2" resolved "https://registry.npmjs.org/braces/-/braces-2.3.2.tgz" @@ -14869,7 +14876,7 @@ cookie-signature@1.0.6: resolved "https://registry.yarnpkg.com/cookie-signature/-/cookie-signature-1.0.6.tgz#e303a882b342cc3ee8ca513a79999734dab3ae2c" integrity sha1-4wOogrNCzD7oylE6eZmXNNqzriw= -cookie@0.6.0, cookie@^0.6.0: +cookie@0.6.0: version "0.6.0" resolved "https://registry.yarnpkg.com/cookie/-/cookie-0.6.0.tgz#2798b04b071b0ecbff0dbb62a505a8efa4e19051" integrity sha512-U71cyTamuh1CRNCfpGY6to28lxvNwPG4Guz/EVjgf3Jmzv0vlDp1atT9eS5dDjMYHucpHbWns6Lwf3BKz6svdw== @@ -14879,6 +14886,11 @@ cookie@^0.5.0: resolved "https://registry.yarnpkg.com/cookie/-/cookie-0.5.0.tgz#d1f5d71adec6558c58f389987c366aa47e994f8b" integrity sha512-YZ3GUyn/o8gfKJlnlX7g7xq4gyO6OSuhGPKaaGssGB2qgDUS0gPgtTvoyZLTt9Ab6dC4hfc9dV5arkvc/OCmrw== +cookie@^0.7.1: + version "0.7.2" + resolved "https://registry.yarnpkg.com/cookie/-/cookie-0.7.2.tgz#556369c472a2ba910f2979891b526b3436237ed7" + integrity sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w== + cookiejar@^2.1.4: version "2.1.4" resolved "https://registry.yarnpkg.com/cookiejar/-/cookiejar-2.1.4.tgz#ee669c1fea2cf42dc31585469d193fef0d65771b" @@ -16309,7 +16321,7 @@ diacritics@^1.3.0: resolved "https://registry.yarnpkg.com/diacritics/-/diacritics-1.3.0.tgz#3efa87323ebb863e6696cebb0082d48ff3d6f7a1" integrity sha1-PvqHMj67hj5mls67AILUj/PW96E= -diff-match-patch@^1.0.0, diff-match-patch@^1.0.4, diff-match-patch@^1.0.5: +diff-match-patch@^1.0.0, diff-match-patch@^1.0.5: version "1.0.5" resolved "https://registry.yarnpkg.com/diff-match-patch/-/diff-match-patch-1.0.5.tgz#abb584d5f10cd1196dfc55aa03701592ae3f7b37" integrity sha512-IayShXAgj/QMXgB0IWmKx+rOPuGMhqm5w6jvFxmVenXKIzRqTAAsbBPT3kWQeGANj3jGgvcvv4yK6SxqYmikgw== @@ -16668,10 +16680,10 @@ elastic-apm-node@3.46.0: traverse "^0.6.6" unicode-byte-truncate "^1.0.0" -elastic-apm-node@^4.7.3: - version "4.7.3" - resolved "https://registry.yarnpkg.com/elastic-apm-node/-/elastic-apm-node-4.7.3.tgz#d819a9030f7321cc858c788f60b383de85461f24" - integrity sha512-x+cQKrXSCz6JgoTFAiBpLlC85ruqZ7sAl+jAS3+DeSmc6ZXLRTwTa2ay2PCGv3DxGLZjVZ+ItzGdHTj5B7PYKg== +elastic-apm-node@^4.8.0: + version "4.8.0" + resolved "https://registry.yarnpkg.com/elastic-apm-node/-/elastic-apm-node-4.8.0.tgz#10d17c3bbd127b8bab9cb264750936a81b4339ad" + integrity sha512-XEfkWWQlIyv72QTCgScFzXWYM2znm/mA+6I8e2DMmr3lBdwemOTxBZw9jExu4OQ2uMc+Ld8wc5bbikkAYp4nng== dependencies: "@elastic/ecs-pino-format" "^1.5.0" "@opentelemetry/api" "^1.4.1" @@ -16682,7 +16694,7 @@ elastic-apm-node@^4.7.3: async-value-promise "^1.1.1" basic-auth "^2.0.1" breadth-filter "^2.0.0" - cookie "^0.6.0" + cookie "^0.7.1" core-util-is "^1.0.2" end-of-stream "^1.4.4" error-callsites "^2.0.4" @@ -16691,7 +16703,7 @@ elastic-apm-node@^4.7.3: fast-safe-stringify "^2.0.7" fast-stream-to-buffer "^1.0.0" http-headers "^3.0.2" - import-in-the-middle "1.11.0" + import-in-the-middle "1.11.2" json-bigint "^1.0.0" lru-cache "10.2.0" measured-reporting "^1.51.1" @@ -20113,10 +20125,10 @@ import-fresh@^3.1.0, import-fresh@^3.2.1, import-fresh@^3.3.0: parent-module "^1.0.0" resolve-from "^4.0.0" -import-in-the-middle@1.11.0: - version "1.11.0" - resolved "https://registry.yarnpkg.com/import-in-the-middle/-/import-in-the-middle-1.11.0.tgz#a94c4925b8da18256cde3b3b7b38253e6ca5e708" - integrity sha512-5DimNQGoe0pLUHbR9qK84iWaWjjbsxiqXnw6Qz64+azRgleqv9k2kTt5fw7QsOpmaGYtuxxursnPPsnTKEx10Q== +import-in-the-middle@1.11.2: + version "1.11.2" + resolved "https://registry.yarnpkg.com/import-in-the-middle/-/import-in-the-middle-1.11.2.tgz#dd848e72b63ca6cd7c34df8b8d97fc9baee6174f" + integrity sha512-gK6Rr6EykBcc6cVWRSBR5TWf8nn6hZMYSRYqCcHa0l0d1fPK7JSYo6+Mlmck76jIX9aL/IZ71c06U2VpFwl1zA== dependencies: acorn "^8.8.2" acorn-import-attributes "^1.9.5" @@ -26600,17 +26612,6 @@ re2js@0.4.2: resolved "https://registry.yarnpkg.com/re2js/-/re2js-0.4.2.tgz#e344697e64d128ea65c121d6581e67ee5bfa5feb" integrity sha512-wuv0p0BGbrVIkobV8zh82WjDurXko0QNCgaif6DdRAljgVm2iio4PVYCwjAxGaWen1/QZXWDM67dIslmz7AIbA== -react-ace@^7.0.5: - version "7.0.5" - resolved "https://registry.yarnpkg.com/react-ace/-/react-ace-7.0.5.tgz#798299fd52ddf3a3dcc92afc5865538463544f01" - integrity sha512-3iI+Rg2bZXCn9K984ll2OF4u9SGcJH96Q1KsUgs9v4M2WePS4YeEHfW2nrxuqJrAkE5kZbxaCE79k6kqK0YBjg== - dependencies: - brace "^0.11.1" - diff-match-patch "^1.0.4" - lodash.get "^4.4.2" - lodash.isequal "^4.5.0" - prop-types "^15.7.2" - react-clientside-effect@^1.2.6: version "1.2.6" resolved "https://registry.yarnpkg.com/react-clientside-effect/-/react-clientside-effect-1.2.6.tgz#29f9b14e944a376b03fb650eed2a754dd128ea3a" @@ -32795,6 +32796,11 @@ xpath@^0.0.33: resolved "https://registry.yarnpkg.com/xpath/-/xpath-0.0.33.tgz#5136b6094227c5df92002e7c3a13516a5074eb07" integrity sha512-NNXnzrkDrAzalLhIUc01jO2mOzXGXh1JwPgkihcLLzw98c0WgYDmmjSh1Kl3wzaxSVWMuA+fe0WTWOBDWCBmNA== +"xstate5@npm:xstate@^5.18.1", xstate@^5.18.1: + version "5.18.1" + resolved "https://registry.yarnpkg.com/xstate/-/xstate-5.18.1.tgz#c4d43ceaba6e6c31705d36bd96e285de4be4f7f4" + integrity sha512-m02IqcCQbaE/kBQLunwub/5i8epvkD2mFutnL17Oeg1eXTShe1sRF4D5mhv1dlaFO4vbW5gRGRhraeAD5c938g== + xstate@^4.38.2: version "4.38.2" resolved "https://registry.yarnpkg.com/xstate/-/xstate-4.38.2.tgz#1b74544fc9c8c6c713ba77f81c6017e65aa89804"